您的位置:首页 > 理论基础 > 数据结构算法

【408 go through】基础数据结构

2018-03-26 15:39 344 查看

0x00 基本内容

数据结构包含 逻辑结构,存储结构,和数据运算。

逻辑结构独立于储存结构,存储结构依赖与逻辑结构来实现。

1.逻辑结构

细分为:

- 线性结构

- 树形结构

- 图结构

- 集合结构

2. 储存结构

又叫物理结构:

- 顺序储存:空间连续。可随机存取。容易产生内存碎片。

- 链式储存:空间不连续,借助地址前后连接。空间利用比较灵活。

- 索引储存:还附加索引项,结构为(key,value/地址)

- 散列储存:利用key直接计算地址(hash)

3.算法

求解问题的步骤,是有限的指令序列。

T(n)描述基本运算的频度,n为问题的规模。

复杂度分析问题消耗资源的数量级

时间复杂度分析T(n)的数量级。

- 最坏时间复杂度。

- 最好时间复杂度。

- 平均时间复杂度:假设所有输入等概率出现的期望运行时间。

空间复杂度分析存储空间的消耗。原地工作指消耗常数空间。

0x01 一般线性表

具有相同数据类型的n个数据元素的有限序列。其中n为表长度。n=0 时表示一个空表。

L=(a1,a2,…ai,…,an) ,1~n .

除了第一个元素,每个元素都有一个直接前驱。同理除了最后一个元素其他都有一个直接后继。

线性逻辑结构,根据储存结构的不同分为顺序存储链式存储

1.顺序表示——顺序表

线性表的顺序存储。使用地址连续的储存单元(数组),依次储存线性表中的数据元素。逻辑相邻的两个元素物理上也相邻

数组下标从0开始,线性表逻辑上从1开始。

数组空间可以静态分配也可以动态分配。(malloc || new )

顺序表最大的特点是支持随机访问,但是因为存储密度大,每次插入和删除都要移动大量元素。

2.链式表示

链式存储的数据存放地址不连续,随机分配,但每个元素(节点)附加了一项地址(指针),将各项数据连接成链

对插入、删除操作比较方便,不需要大量移动数据元素。但是只能支持顺序访问 ,每次都要从头开始遍历到需要找的那个元素。

a. 单链表

连接方向为单向

typedef struct LNode{
ElemType data ;
struct LNode *next ;
}LNode, *LinkList ;


通常直接使用一个头指针 * LinkList 来表示一个链表,指向链表的第一个结点。如果指针=NULL ,则表示一个空表。

另外,为了统一所有结点以及空表与非空表在代码上的操作,可以在第一个元素之前给单链表附加一个头结点 , 头节点的next指针指向链表第一个元素。

这样,头结点就成为了单链表上的第一个结点(data 不包含任数据)。头指针始终指向头结点,头结点的next 指向第一个元素 , 此时如果为空表 ,则头指针的next==NULL。

单链表建立方法分为头插法和尾插法

头插法: 从空表开始,每次插入的数据都放在头结点之后 。

尾插法: 每次插入位置都在表尾,需要在插入时维持一个尾指针指向链表尾结点。

通常使用尾插法,另外也可以通过进行尾插法然后交换数据变为逻辑上的头插法

b. 双链表

单链表,方向单一,只能后向遍历进行访问,当需要访问前驱节点时就需要重新遍历。为了克服该缺点引入双向链表。结点中有两个指针prior 和 next 。

typedef struct DNode{
ElemType data;
struct  DNode *prior ,* next;
}DNode , *DLinkList;


c.循环链表

循环单链表最后一个结点的指针不是NULL,而是改为指向头结点,形成环。表中没有值为NULL的next指针。

循环双链表中类似,末尾结点next指向头结点,头结点的prior 指向末尾结点。

d. 静态链表

借助数组来描述链式结构 。因此需要预先分配较大一片连续空间。

typedef struct {
ElemType data;
int next;
} SLinkList[MaxSize] ;


指针使用数组的下表来代替,SLinkList[0]为链表的“头结点”,其next指向第一个元素。末尾节点的next使用 -1 表示结束。 显然 SLinkList[0]->next=-1 表示空表。

在不支持指针的语言中这是比较巧妙的设计。

0x02 栈Stack(特殊的、受限的线性表)

首先栈属于线性表的一种,但是受到一些限制——只能在顶部进行插入和删除(FILO先进后出)

逻辑上不允许遍历,只能对栈顶进行访问

1. 顺序栈

事先在创建时分配一组连续的空间(数组)存放数据元素,指针指向栈顶

typedef struct{
Elemetype data[MaxSize];
int top;    //非链式储存当然是用数组下表表示“指针”
}SqStack;


栈空时 top = -1 ,栈满 top=MaxSize-1 。

2. 共享栈

两个栈共用一组连续空间,其中一个栈从下向上生长,另一个从上向下生长,节约空余空间。当两个栈top1-top0=1时,则表明栈的空间已经满了。

top0的初始值为-1, top1为* MaxSize* 。

3. 链式栈

链式存储的栈,通常使用单链表实现。

由于是链式存储,空间使用不受限制,因而不存在上溢。

typedef struct Linknode{
Elemtype data;
struct Linknode * next;
} *LinkStack;


因为不需要遍历操作,所以所有操作都在表头实现,只需要将top指针指向表头,插入时为前插法即可。

4. extra. 出栈序列问题

入栈序列为 (1,2,3,… ,n),共可以有多少种出栈序列 ?

这是一个关于卡特兰(Catalan)数的问题。共有C(n,2n)/n+1个可能序列。

第i个出栈元素为Xi,则序列后边所有小于Xi的元素必然递减排列。

实际上Xi出栈时,则出栈序列的后续所有比Xi的小元素必然还在栈中,并自栈底向栈顶递增,所以出栈必定递减排列。

0x03 队列Queue

同样也是首先的线性表,只能先进先出(FIFO)——在队首进队尾出。

不能队中间的元素进行操作,只能访问队头,队头出队,队尾入队。

1.顺序存储的队列

数组中,使用两个下标frontrear 分别作为队头位置和队尾的下一个位置 。这样当队列为空时 rear=front ,队长为 rear-front

但是当rear=Maxsize 之后,虽然数组空间仍可能有余但是整个队列已经不能再进行入队操作了。这就产生了假上溢

引入循环队列。

为了解决上述的假上溢问题,对rear 和 front 的加减操作引入了mod 运算,令其得以循环。

初始时:rear=front =0

出队 : front = (front+1)%MaxSize

入队 : rear = (rear+1)%MaxSize

求队长:rear+MaxSize-front%MaxSize

不过这样又产生了一个问题:队满队空时都是rear=front

但是仔细观察我们会发现一个事实,进行mod运算之后队长的表示范围只支持 0~MaxSize-1 。那么我们牺牲一个元素的空间,约定 队长=MaxSize-1 表示队伍已满即可。

2. 链式队列

实际上,这是一个带有头指针和尾指针的单链表,利用头指针进行出队删除操作,利用尾指针进行入队插入操作。

typedef struct {
Elemtype data;
struct LinkNode *next;
}LinkNode;

typedef struct {
LinkNode * front,*rear;
}LinkQueue;


当front=NULL ,rear=NULL 时,队列为空。

使用头结点时,front始终指向头结点,当front=rear时即表示为空表。

LinkQueue 可以增加一个整型表示队列元素数量,值为0时为空表。

3.双端队列

两端都可以进行入队和出队操作。

输出受限的双端队列:只有一端能进行出队操作。

输入受限的双端队列:只有一端能进行入队操作。

受限的双端队列出队问题:

入队序列为1,2,3,4。

能由输人受限的双端队列得到,但不能由输出受限的双端队列得到的序列为:4132

能有输出受限的双端队列得到,但不能由输入受限得到的序列为:4213

既不能由输出受限,也不能由输入受限的双端队列得到的出队序列为:4231 .

0x04. 栈和队列,数组的一些应用

1. 栈,递归,括号匹配和表达式

递归是一种重复自身调用自身的方法,通常用于将问题变为更小规模的相同问题进行求解,再组合结果以得到当前规模下的解。

递归的代码虽然简洁易懂,但是效率并不高(出入栈状态保存消耗,子状态可能会重复求解,层数过多也容易产生栈溢出)。

通常我们可以将递归问题转为相同的非递归问题,由于递归本身是用栈实现的,我们可以通过栈来模拟递归过程,通常对于单向递归尾递归也可以直接自下往上用迭代求解,以得到当前参数下的解。

1)括号匹配问题(求解括号嵌套序列的正确性):

主要思想是,顺序访问括号序列,找到一个左括号时,:

指针向右进1,寻找对应的右括号。

如果又出现一个左括号,从该指针位置开始递归调用自身,如果该调用返回flase,则自己也返回false。否则从1开始,指针继续向右。

如果此时出现右括号不匹配,返回false。

如果此时括号匹配,返回true。

如果指针到达末尾仍未匹配到,返回false。

将该方法定义为match( i ),则一开始调用match(0)即可,返回false即序列错误。

2)运算表达式问题

中缀表达式转换为后缀表达式问题

中缀表达式即是我们常见的表达式形式 ( e.g. A+B*C

将其转换为后缀表达式: ABC*+

转化过程需要利用一个栈保存操作符保证运算优先级,一般过程见王道数据结构。

后缀表达式求值

同样利用一个栈,从左往右,如果遇到操作数则入栈,如果遇到操作符,则将栈弹出两次,得到两个操作数根据操作符进行运算,把结果压回栈中,直到最后到达末尾。

2. 队列,层次遍历(广度优先),缓冲队列

层次遍历二叉树步骤:

根节点入队。

队列出队一个节点,访问其中数据,若左节点存在,讲左节点入队,右节点同样。

重复步骤2,直到队列为空。

3.矩阵的压缩存储

矩阵一般用一个二维数组来存储。

元素具有某些特定分布规律的矩阵,可以通过一些手段进行压缩以节省空间。

对称矩阵

由于其对称性,可以只保存其中的上(下)三角元素,到一个一维数组B[n(n+1)/2]中。

上(下)三角矩阵

除了三角区其余全部为同一常量。所以相比对称矩阵,需要多一个单位空间来保存该常量。所以耗费B[n(n+1)/2+1]。

三对角矩阵

带状矩阵。三条对角线。

稀疏矩阵

非零元素只占很小的一部分,直接使用二维数据会浪费大量空间。所以可以使用三元组(i,j,value)的列表来储存非零元素。但是这样会失去随机访问的特性,增加访问、查找的复杂度。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  408