数据结构
目录
前言
提示:学习数据结构,扎实自己的算法基础,提升自己的编程思维
一、线性表
线性表是具有相同类型的n(n>=0)个元素的有限序列,其中n为表长,当n=0时,线性表是一个空表。用L命名线性表,则其一般表示为:
L = ( a1,a2,a3,..,ai,ai+1,...,an )
式中a1是唯一一个“第一个”数据元素,又称为表头元素,an是“最后一个”数据元素,又称表尾元素。除第一个元素之外,每个元素有且只有一个直接前驱,除最后一个元素外,每个元素有且只有一个直接后继。
线性表的特点:
- 表中元素的个数有限。
- 表中元素具有逻辑顺序性,表中元素有其先后次序。
- 表中元素都是数据元素,每个元素都是单个元素。
- 表中元素的数据类型相同,这意味着每个元素占有相同大小的存储空间。
- 表中元素具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容。
注意:线性表是一种逻辑结构,表示元素之间一对一的相邻关系,顺序表和链表是指存储结构,两者属于不同层面的概念,不要混淆。
1.1 线性表的顺序表示(顺序表)
1.1.1 定义:
它是一组地址连续的存储单元依次存储数据表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。
线性表中元素的位序从1开始,而数组中元素的下标是从0开始的。
设线性表的元素类型为ElemType,则线性表的顺序存储描述为:
#define MaxSize 50 //定义线性表的最大长度
typedef struct {
ElemType data[MaxSize]; //顺序表的元素
int length; //顺序表的类型定义
}SqList;
以上为线性表静态分配,一旦空间占满,再加入新的数据会产生溢出,进而导致程序崩溃。
而在动态分配时,存储数组的空间是在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满,就另外开辟一块更大的存储空间,用来替换原来的存储空间,从而达到扩充存储空间的目的。而不需要为线性表一次性划分所有空间
#define InitSize 100 //表长度的初始定义
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize,length; //数组的最大容量和当前个数
}SeqList; //动态分配数组顺序表的类型定义
C语言的动态初始分配语句:
L.data=(ElemType *)malloc(sizeof(ElemType)*InitSize);
C++的初始动态分配空间语句:
L.data = new ElemType[InitSize];
动态分配并不是链式存储,它依然是顺序存储结构,不改变物理结构,随机存取,分配的大小空间在运行时决定
顺序表的特点:
- 随机访问,通过首地址和元素序号可以在O(1)内找到指定的元素
- 存储密度高,每个结点只存储数据元素。
- 逻辑相邻也物理相邻, 所以在插入和删除操作时需要移动大量元素。
1.1.2 顺序表的基本操作实现
1.插入:在顺序表L的i位置(1<=i<=L.length+1)插入新元素e,若i的输入不合法,则返回false,
表示插入失败,否则,将顺序表第i个元素及其后的所有元素右移一个位置,腾出一个空位置插入新元素e,顺序表长度加1,返回值为true.
bool InsertList(SeqList &L, int i, ElemType e) {
if(i<1 || i> L.length+1) {
return false;
}
if(L.length > MaxSize) {
return false;
}
for(int j = L.length;j>=i;j--) { //将第i个元素以及之后的元素后移
L.data[j] = L.data[j-1];
}
L.data[i-1] = e;
L.length++;
return true;
}
- 最好情况:在表尾插入,元素后移语句不执行,时间复杂度O(1)
- 最坏情况:在表头插入,元素后移执行n次,时间复杂度O(n)
- 平均情况:1/(n+1) * n(n+1)/2 = n/2 ,其复杂度为O(n)
2.删除
bool DeleteList(SeqList &L, int i, ElemType e) {
if(i<1 || i>L.length) {
return false;
}
e = L.data[i-1]; //将被删除的元素赋值给e
for(int j=i;j<L.length;j++) { //元素前移
L.data[j-1]=L.data[j];
}
L.length--;
return true;
}
- Best:删除表尾元素,无需移动元素,时间复杂度O(1)
- Worst:删除表头元素,需要移动n-1个元素,时间复杂度为O(n)
- 平均情况:1/n * n(n-1)/2 = (n-1)/2 时间复杂度:O(n)
3.查找第一个元素值等于e的元素,返回其顺序
int LocateElem(SeqList L, ElemType e) {
int i;
for(i=0;i<L.length;i++) {
if(L.data[i] == e) {
return i+1; // 下标为i的元素值等于e,返回其位序i+1
}
}
return 0; // 退出循环,说明查找失败
}
- 最好情况:元素位于表头,只比较一次,时间复杂度为O(1)
- 最坏情况:查找的元素位于表尾或不存在。需要比较n次,时间复杂度为O(n)
- 平均情况:1/n * n(n+1)/2 = (n+1) / 2; O(n)
1.2 线性表的链式表示
链式存储线性表,不需要使用地址连续的存储单元(不要求逻辑上相邻的元素在物理位置上也相邻),它通过链建立数据元素之间的逻辑关系,因此插入和删除操作不需要移动元素,而只需要修改指针,但是也失去了顺序表随机存取的优势。
1.2.1 单链表的定义
typedef struct LNode{
ElemTpe data;
struct LNode *next;
}LNode,*LinkList;
1.2.2 单链表的基本操作
1.采用头插法建立单链表
该方法从一个空表开始,生成新的结点,并将读取到的数据放到新节点的数据域中,然后将新节点插入到当前链表的表头,即头结点之后。
LinkList HeadInsert_List(LinkList &L) { //逆向建立单链表
LNode *s;
int x;
L = (LinkList)malloc(sizeof(LNode)); //创建头结点
L->next = null; //初始化为空链表
scanf("%d",&x);
while(x!=9999) { //输入9999结束
s = (LNode *)malloc(sizeof(LNode)); //创建新节点
s->data = x;
s->next = L->next;
L->next = s;
scanf("%d",&x);
}
return L;
}
采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素的顺序时相反的,每个结点插入的时间为O(1),设单链表的总长度为n,则总时间复杂度为O(n)。
2 尾插法建立链表
LinkList TailInsertList(LinkList &L) { //正向建立单链表
int x; //设置元素类型为整型
L = (LinkList)malloc(sizeof(LNode));
LNode *s,*r=L; //r为表尾指针
scanf("%d",&x);
while(x!=9999) { // 输入9999表示结束
s=(LNode *)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s; //r指向新的表尾结点
scanf("%d",&x);
}
r->next = NULL; //尾结点指针置空
return L;
}
3 按序号查找结点值
在单链表中从第一个结点出发,顺指针next域逐个往下搜索,直到找到第i个结点为止,否则返回最后一个结点指针域NULL。
LNode *GetElem(LinkList L, int i) {
int j = 1; //计数,初始值为1
LNode *p = L->next; //头结点指针赋给p
if(i==0) {
retrurn L;
}
if(i<1) {
return NULL;
}
while(p&&j<i) {
p = p->next;
j = j++;
}
return p;
}
时间复杂度:O(n)
4.按值查找表结点
从单链表的第一个结点开始,由前向后依次比较表中各个结点数据域的值,若某个结点数据域的值等于给定值e,则返回该节点的指针,若整个单链表中没有这样的节点,则返回NULL。
LNode *LocateElem(LinkList L,ElemType e){
LNode *p = L -> next;
while(p!=NULL && p->data!=e)
p=p->next;
return p;
}
5.插入节点操作
插入节点操作将值x的新节点插入到单链表的第i个位置上,先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i-1个结点,再在其后插入新结点。
思路:调用按序号查找方法GetElem(L,i-1),查找第i-1个结点,假设返回的第i-1个结点为*p,然后令新结点 *s 的指针域指向 *p 的后继结点,再令结点 *p 的指针域指向新插入的结点 *s。其操作过程:
p = GetElem(L,i-1); //查找插入位置的前驱结点
s -> next = p -> next;
p -> next = s;
s -> next = p -> next 语句 与 p -> next = s语句不能颠倒,否则,若先执行p->next -s 语句之后,指向其原来后继的指针就不存在,在执行s->next = p->next,相当于执行了 s->next = s,显然是错误的。该算法主要的时间开销在于查找第 i-1 个元素,时间复杂度为 O(n) 。若在给定的结点后面插入新结点,这时间复杂度为 O(1)。
对某一结点进行前插操作:
前插操作是指在某一结点的前面插入一个新结点,与后插操作刚好相反,在单链表的操作中,通常采用后插法。
方法一:O(n)
p = GetElem(L, i-1);
s->next = p->next;
p->next = s;
方法二:将带插入结点插入到p结点的后面,然后将s结点和p结点的数据域互换,实现前插的效果,具体实现如下:
s->next = p->next;
p->next = s;
temp = p->data;
p->data = s->data;
s->data = temp;
6.删除结点操作
删除结点操作是将单链表的第 i 个结点删除。先检查删除位置的合法性,后查找表中第 i-1 个结点,即被删除的结点的前驱结点,再将其删除,操作过程如下所示:
*p 为找到的被删除结点的前驱结点,修改 *p 的指针域,即将 *p 的指针域 next 指向 被删除结点 *q的下一个结点。
p = GetElem(L,i-1);
q = p->next;
p->next = q->next;
free(q);
时间复杂度:O(n)
扩展:删除 *p 结点
将其后继结点的值赋予自身,然后删除后继结点,然后删除后继结点,因为避免了查找操作,所以使得时间复杂度为O(1)
q=p->next;
p->data = p->next->data;
p->next = q->next;
free(q);
7.求表长操作:计算单链表中数据结点(不含头结点)的个数,需要从第一个结点开始顺序一次访问表中的每个结点,为此需要设置一个计数器变量,每访问一个结点,计数器加1,直到访问到空结点为止,算法的时间复杂度为O(n)。
1.2.3 双链表
单链表的缺陷:由于单链表只有一个指向后继的指针,使得单链表只能从头结点依次顺序地向后面遍历。要访问某个结点的前驱结点(插入,删除),只能从头开始遍历,访问后继结点的时间复杂度为O(1),访问前驱结点的时间复杂度为O(n)。
由此,双链表横空出世,其结点有两个指针priot和next,分别指向其前驱结点和后继结点,如图所示
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinklist;
1.插入操作:
s->next = p->next;
p->next->priod = s;
s->priod = p;
p->next = s;
注:1和2必须在4之前完成。
2.删除操作
p->next = q->next;
q->next->priod = p;
free(q)
1.2.4 循环链表
循环单链表:
循环链表和单链表的区别在于,表中的最后一个节点的指针不是NULL,而是改为指向头结点。
- 在循环链表中,表尾结点*r的next域指向L,故表尾没有指针域为NULL的结点,因此,判定循环单链表的判空条件为它是否指向头指针。
- 循环单链表在任何一个位置进行插入和删除操作都是等价的。
- 对循环单链表设置尾指针的效率更高,因为若是设置头指针,对表为进行操作时间复杂度为O(n),而若是尾指针,r->next即为头指针,对于表头和表尾进行操作的时间复杂度均为O(1)
2.循环双链表
在循环双链表中,某结点 *p 为尾结点时,p->next ==L; 当循环双链表为空表时,其头结点的piror域和next域都等于L。
1.2.5 静态链表
静态链表借助数组来描述线性表的链式存储结构,结点域也有数据域data和指针域next,与上文所述不同的是,这里的指针是结点的相对地址(数组下标),又称浮标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。
静态链表和单链表之间的对应关系:
结构类型描述如下:
#define MaxSize 60 //静态链表最大长度
typedef struct{ //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
静态链表以next == -1 作为其结束的标志,静态链表的插入、删除操作与动态链表相同。只需要修改指针,而不需要移动元素。总体来说,静态链表没有单链表使用方便。
1.3 顺序表 & 链表
顺序表(访问) | 链表(插入,删除) | |
---|---|---|
存取(读写)方式 | 顺序存取,随机存取,一次访问 | n次访问,从表头顺序存取元素 |
逻辑结构与物理结构 | 逻辑上相邻的元素,物理上也相邻 | 逻辑上相邻的元素物理上比一定相邻,对应的 逻辑关系通过指针连接来表示 |
查找、插入、删除操作 | 无须:O(n) 有序(折半查找):O(logn) | O(n) |
空间分配 | 效率低 | 结点空间只在需要时申请分配,只要内存有空间 就可以分配,操作灵活高效。 |
a.基于存储考虑 | 难以估计线性表的长度或存储规模 时,不宜采用顺序表 | 不需要实现估计规模,但链表的存储密度较低,显然 其存储密度是小于1的。 |
b.基于运算的考虑 | 按序号访问:O(1) 插入删除:平均移动表中一半元素, O(n) | 按序号访问:O(n) 插入删除:优于顺序表 |
c.基于环境考虑 | 顺序表容易实现,因为任何高级语言中都 有数组类型 | 链表是基于指针的 |