数据结构 宏定义 在本笔记中用到的宏定义,头文件为define.h
1 2 3 4 5 6 7 8 9 10 #ifndef __DEFINE_H #define __DEFINE_H #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 typedef int Status;#endif
第一章:线性表
线性表的基本操作
InitList(&L) :初始化表。构造一个空的线性表L,分配内存空间。
DestroyList(&L) :销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
ListInsert(&L,i,e) :插入操作。在表L中的第i个位置上插入指定元素e。
ListDelete(&L,i,&e) :删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
LocateElem(L,e) :按值查找操作。在表L中查找具有给定关键字值的元素。
GetElem(L,i) :按位查找操作。获取表L中第i个位置的元素的值。
其他常用操作
Length(L) :求表长。返回线性表L的长度,即L中数据元素的个数。
PrintList(L) :输出操作。按前后顺序输出线性表L的所有元素值。
Empty(L) :判空操作。若L为空表,则返回true,否则返回false。
Tips
对数据的操作(记忆思路)—— 创销、增删改查
C 语言函数的定义 —— <返回值类型> 函数名(<参数1类型> 参数1, <参数2类型> 参数2, ......)
实际开发中,可根据实际需求定义其他的基本操作
函数名和参数的形式、命名都可改变(Reference:严蔚敏版《数据结构》)
==什么时候要传入引用 “&”== —— ==对参数的修改结果需要 “带回来”==
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> void test (int &x) { x = 1024 ; cout << "test 内部:x = " << x << endl; } int main () { int x = 1 ; cout << "调用 test 前:x = " << x << endl; test (x); cout << "调用 test 后:x = " << x << endl; return 0 ; }
1.1顺序表的定义 (Sequence List)
特点:逻辑上相邻的数据元素,其物理次序也是相邻的
线性表中第(i + 1)个数据元素的存储位置LOC(ai+1)和第i个元素满足下列关系: LOC(ai+1) = LOC(ai) + l LOC(ai) = LOC(ai) + (i - 1) * l 其中,l:代表每个元素所占的存储单元。
顺序表的存储结构:
1 2 3 4 5 6 #define SQLMAXSIZE 100 typedef int SqlElemType;typedef struct __Sqlist { SqlElemType *base; int length; } Sqlist;
1.1.1初始化 1.静态分配 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <stdio.h> #define MaxSize 10 typedef struct { int data[MaxSize]; int length; }SqList; void InitList (SqList &L) { for (int i=0 ;i<MaxSize;i++){ L.data[i]=0 ; } L.length=0 ; } int main () { SqList L; InitList(L); for (int i=0 ;i<MaxSize;i++){ printf ("data[%d]=%d\n" ,i,L.data[i]); } return 0 ; }
2.动态分配 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <stdio.h> #include <stdlib.h> #define InitSize 10 typedef struct { int *data; int MaxSize; int length; }SeqList; void InitList (SeqList &L) { L.data =(int *)malloc (InitSize*sizeof (int )) ; L.length=0 ; L.MaxSize=InitSize; } void IncreaseSize (SeqList &L,int len) { int *p=L.data; L.data=(int *)malloc ((L.MaxSize+len)*sizeof (int )); for (int i=0 ;i<L.length;i++){ L.data[i]=p[i]; } L.MaxSize=L.MaxSize+len; free (p); } int main (void ) { SeqList L; InitList(L); IncreaseSize(L,5 ); return 0 ; }
1.2顺序表的基本操作 1. 插入操作:平均时间复杂度O(n) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 bool ListInsert (SqList &L, int i, int e) { if (i<1 ||i>L.length+1 ) return false ; if (L.length>MaxSize) return false ; for (int j=L.length; j>=i; j--){ L.data[j]=L.data[j-1 ]; } L.data[i-1 ]=e; L.length++; return true ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <stdio.h> #define MaxSize 10 typedef struct { int data[MaxSize]; int length; } SqList; void InitList (SqList &L) { for (int i = 0 ; i < MaxSize; i++) { L.data[i] = 0 ; } L.length = 0 ; } bool ListInsert (SqList &L, int i, int e) { if (i < 1 || i > L.length + 1 ) return false ; if (L.length >= MaxSize) return false ; for (int j = L.length; j >= i; j--) L.data[j] = L.data[j - 1 ]; L.data[i - 1 ] = e; L.length++; return true ; } int main () { SqList L; InitList(L); ListInsert(L, 3 , 3 ); return 0 ; }
i 范围是 [1, length + 1] 的原因
顺序表的位序 (即参数 i)是从 1 开始计数的(而数组下标从 0 开始)。
当 i = 1 时:表示在顺序表的第一个位置 (对应数组下标 0)插入元素,这是 “最前面插入” 的情况。
当 i = length + 1 时:表示在顺序表的最后一个元素之后 插入(相当于 “尾插”)。
比如当前 length = 6,则 i 最大可以取 7(即 length + 1 = 6 + 1 = 7),对应在数组 data[6] 的位置插入元素(此时数组前 6 个位置 data[0]~data[5] 已有元素)。
2. 删除操作:平均时间复杂度O(n) 1 2 3 4 5 6 7 8 9 10 11 12 13 bool LisDelete (SqList &L, int i, int &e) { if (i<1 ||i>L.length) return false ; e = L.data[i-1 ]; for (int j=L.length; j>=i; j--){ L.data[j-1 ]=L.data[j]; } L.length--; return true ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include <stdio.h> #define MaxSize 10 typedef struct { int data[MaxSize]; int length; } SqList; void InitList (SqList &L) { L.length = 0 ; for (int i = 0 ; i < MaxSize; i++) { L.data[i] = 0 ; } } bool ListDelete (SqList &L, int i, int &e) { if (i < 1 || i > L.length) return false ; e = L.data[i - 1 ]; for (int j = i; j < L.length; j++) { L.data[j - 1 ] = L.data[j]; } L.length--; return true ; } int main () { SqList L; InitList(L); int e = -1 ; if (ListDelete(L, 3 , e)) printf ("已删除第3个元素,删除元素值为=%d\n" , e); else printf ("位序i不合法,删除失败\n" ); return 0 ; }
3. 按位查找:(获取L表中第i个位置的值):平均时间复杂度O(1)
静态分配
#define MaxSize 10
typedef struct {
ElemType data[MaxSize];
int length;
}SqList;
ElemType GetElem (SqList L, int i) {
return L.data[i-1 ];
}
<!--code10 -->
时间复杂度:O (1)
由于顺序表的各个数据元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第 i 个元素 ——“随机存取” 特性
4. 按值查找:平均时间复杂度O(n) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #define InitSize 10 typedef struct { ElemType *data; int MaxSize; int length; } SeqList; int LocateElem (SeqList L, ElemType e) { for (int i = 0 ; i < L.length; i++) if (L.data[i] == e) return i + 1 ; return 0 ; }
1.1.6销毁,清空,检查为空 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 Status SqlDelete (Sqlist *L, int position, SqlElemType *e) { if (position < 1 || position > L->length) { return ERROR; } *e = L->base[position - 1 ]; for (int i = position; i < L->length; i++) { L->base[i - 1 ] = L->base[i]; } L->length--; return OK; } Status SqlDestroy (Sqlist *L) { if (!L->base) { return ERROR; } free (L->base); L->base = NULL ; L->length = 0 ; return OK; } void SqlClear (Sqlist *L) { L->length = 0 ; } Status SqlIsEmpty (Sqlist *L) { return (L->length == 0 ) ? TRUE : FALSE; }
L->length 当 L 是指针变量 (指向结构体 / 类的指针)时,使用 -> 运算符访问其成员。 例如:Sqlist *L;(L 是指向 Sqlist 结构体的指针),此时需用 L->length 访问 length 成员。
L.length 当 L 是结构体 / 类的实例变量 (非指针)时,使用 . 运算符访问其成员。 例如:Sqlist L;(L 是 Sqlist 结构体的实例),此时需用 L.length 访问 length 成员。
1.3 线性表的链式表示 1.3.1单链表 1. 单链表的定义 定义: 线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。
1 2 3 4 typedef struct LNode { ElemType data; struct LNode *next ; }LNode, *LinkList;
可以利用typedef关键字——数据类型重命名:type<数据类型><别名>
next:是一个指针,指向struct LNode类型的数据,用于存放下一个节点的地址,通过这个指针将各个节点连接起来,形成链表
LNode:是struct LNode的别名,使用时可以直接用LNode代替struct LNode来定义节点变量
*LinkList:是struct LNode *的别名,表示指向链表节点的指针,通常用于表示整个链表(指向链表的头节点)
单链表的两种实现方式:
不带头结点的单链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 typedef struct LNode { ElemType data; struct LNode *next ; }LNode, *LinkList; bool InitList (LinkList &L) { L = NULL ; return true ; } void test () { LinkList L; InitList(L); } bool Empty (LinkList L) { if (L == NULL ) return true ; else return false ; }
头结点:代表链表上头指针指向的第一个结点,不带有任何数据。
带头结点的单链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 typedef struct LNode { ElemType data; struct LNode *next ; }LNode, *LinkList; bool InitList (LinkList &L) { L = (LNode*) malloc (sizeof (LNode)); if (L == NULL ) return false ; L -> next = NULL ; return true ; } void test () { LinkList L; InitList(L); } bool Empty (LinkList L) { if (L->next == NULL ) return true ; else return false ; }
带头结点和不带头结点的比较:
不带头结点:写代码麻烦!对第一个数据节点 和后续数据节点 的处理需要用不同的代码逻辑 ,对空表和非空表的处理也需要用不同的代码逻辑; 头指针指向的结点用于存放实际数据;
带头结点:头指针指向的头结点不存放实际数据 ,头结点指向的下一个结点才存放实际数据;
2. 按位序插入(带头结点): ListInsert(&L, i, e):在表L中的第i个位置上插入指定元素e = 找到第i-1个结点(前驱结点),将新结点插入其后;其中头结点可以看作第0个结点,故i=1时也适用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 typedef struct LNode { ElemType data; struct LNode *next ; }LNode, *LinkList; bool ListInsert (LinkList &L, int i, ElemType e) { if (i<1 ) return False; LNode *p; int j=0 ; p = L; while (p!=NULL && j<i-1 ){ p = p->next; j++; } if (p==NULL ) return false ; LNode *s = (LNode *)malloc (sizeof (LNode)); s->data = e; s->next = p->next; p->next = s; return true ; }
p 是指向 LNode 类型结构体的指针,而 LNode 结构体中包含 next 成员(struct LNode *next;),所以可以通过 p->next 来访问该结点的 next 元素,进而操作链表的下一个结点
s->next = p->next; //将新结点 s 的指针域赋值为 p->next,使 s 的下一个结点指向 p 原本的下一个结点
p->next = s; //更新指针 p 的指针域,使其指向新结点 s,完成 “p → s → 原 p 的下一个结点” 的链接
==平均时间复杂度:O(n)==
3. 按位序插入(不带头结点) ListInsert(&L, i, e):在表L中的第i个位置上插入指定元素e = 找到第i-1个结点(前驱结点),将新结点插入其后; 因为不带头结点,所以不存在“第0个”结点,因此!i=1 时,需要特殊处理——插入(删除)第1个元素时,需要更改头指针L;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 typedef struct LNode { ElemType data; struct LNode *next ; }LNode, *LinkList; bool ListInsert (LinkList &L, int i, ElemType e) { if (i<1 ) return false ; if (i==1 ){ LNode *s = (LNode *)malloc (size of(LNode)); s->data =e; s->next =L; L=s; return true ; } LNode *p; int j=1 ; p = L; while (p!=NULL && j<i-1 ){ p = p->next; j++; } if (p==NULL ) return false ; LNode *s = (LNode *)malloc (sizeof (LNode)); s->data = e; s->next = p->next; p->next = s; return true ; }
4. 指定结点的后插操作: InsertNextNode(LNode *p, ElemType e): 给定一个结点p,在其之后插入元素e; 根据单链表的链接指针只能往后查找 ,故给定一个结点p,那么p之后的结点我们都可知,但是p结点之前的结点无法得知;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 typedef struct LNode { ElemType data; struct LNode *next ; }LNode, *LinkList; bool InsertNextNode (LNode *p, ElemType e) { if (p==NULL ){ return false ; } LNode *s = (LNode *)malloc (sizeof (LNode)); if (s==NULL ) return false ; s->data = e; s->next = p->next; p->next = s; return true ; } bool ListInsert (LinkList &L, int i, ElemType e) { if (i<1 ) return False; LNode *p; int j=0 ; p = L; while (p!=NULL && j<i-1 ){ p = p->next; j++; } return InsertNextNode(p, e) }
5. 指定结点的前插操作
思想:设待插入结点是s ,将s 插入到p 的前面。我们仍然可以将s 插入到p 的后面。然后将p->data与s->data交换,这样既能满足了逻辑关系,又能是的时间复杂度为==O(1)==.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 bool InsertPriorNode (LNode *p, ElenType e) { if (p==NULL ) return false ; LNode *s = (LNode *)malloc (sizeof (LNode)); if (s==NULL ) return false ; s->next = p->next; p->next = s; s->data = p->data; p->data = e; return true ; }
6. 按位序删除节点(带头结点)
ListDelete(&L, i, &e) :删除操作,删除表L中第i个位置的元素,并用e返回删除元素的值;头结点视为“第0个”结点;
思路:找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 typedef struct LNode { ElemType data; struct LNode *next ; }LNode, *LinkList; bool ListDelete (LinkList &L, int i, ElenType &e) { if (i<1 ) return false ; LNode *p; int j=0 ; p = L; while (p!=NULL && j<i-1 ){ p = p->next; j++; } if (p==NULL ) return false ; if (p->next == NULL ) return false ; LNode *q = p->next; e = q->data; p->next = q->next; free (q) return true ; }
时间复杂度分析:
最坏、平均时间复杂度:O(n)
最好时间复杂度:删除第一个 结点 O(1)
7. 指定结点的删除
删除结点p,需要修改其前驱结点的 next 指针
方法1:传入头指针,循环寻找p的前驱结点
方法2:偷天换日(类似于结点前插的实现)
1 2 3 4 5 6 7 8 9 10 bool DeleteNode (LNode *p) { if (p==NULL ) return false ; LNode *q = p->next; p->data = p->next->data; p->next = q->next; freem(q); return true ; }
But
如果p是最后一个结点:q指针是指向p指针的next,也就是指向了NULL,执行到p->data=p->next->data,就会出错,因为q结点没有指向具体的结点,不能取得data域,就会出现空指针的错,只能用法一:
只能从表头开始依次寻找p的前驱,时间复杂度O(n)
单链表的局限性:无法逆向检索,有时候不太方便
如果各个结点有前面的指针:双链表
8. 按位查找 GetElem(L, i): 按位查找操作,获取表L中第i个位置的元素的值;
1 2 3 4 5 6 7 8 9 10 11 12 13 LNode * GetElem (LinkList L, int i) { if (i<0 ) return NULL ; LNode *p; int j=0 ; p = L; while (p!=NULL && j<i){ p = p->next; j++; } return p; }
平均时间复杂度O(n)
9. 按值查找 LocateElem(L, e):按值查找操作,在表L中查找具有给定关键字值的元素;
1 2 3 4 5 6 7 8 LNode * LocateElem (LinkList L, ElemType e) { LNode *P = L->next; while (p!=NULL && p->data != e){ p = p->next; } return p; }
平均时间复杂度O(n)
10. 求单链表的长度 Length(LinkList L):计算单链表中数据结点(不含头结点)的个数,需要从第一个结点看是顺序依次访问表中的每个结点。
1 2 3 4 5 6 7 8 9 int Length (LinkList L) { int len=0 ; LNode *p = L; while (p->next != NULL ){ p = p->next; len++; } return len; }
平均时间复杂度O(n)
11. 尾插法建立单链表(时间复杂度O(n))
类似按位序插入,每次把元素插入表尾
但是每次插入都要从头遍历,平均时间复杂度O(n^2^)
实际上没必要每次从表头开始遍历,可以设置一个表尾指针,指向表尾最后一个数据结点,类似后插操作
思路:每次将新节点插入到当前链表的表尾,所以必须增加一个尾指针r,使其始终指向当前链表的尾结点。好处:生成的链表中结点的次序和输入数据的顺序会一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 LinkList List_TailInsert (LinkList &L) { int x; L = (LinkList)malloc (sizeof (LNode)); LNode *s, *r = L; scanf ("%d" , &x); while (x!=9999 ){ s = (LNode *)malloc (sizeof (LNode)); s->data = x; r->next = s; r = s scanf ("%d" , &x); } r->next = NULL ; return L; }
平均时间复杂度O(n)
12. 头插法建立单链表(平均时间复杂度O(n))
对头结点的后插操作
思路:每次都将生成的结点插入到链表的表头。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 LinkList List_HeadInsert (LinkList &L) { LNode *s; int x; L = (LinkList)malloc (sizeof (LNode)); L->next = NULL ; scanf ("%d" , &x); while (x!=9999 ){ s = (LNode *)malloc (sizeof (LNode)); s->data = x; s->next = L->next; L->next = s; scanf ("%d" , &x); } return L; }
发现插入的是跟输入的是反的
应用:
链表的逆置: (利用头插法)
核心代码逻辑不变,取数据元素的时候,不适用scanf,而是通过一个指针去扫描,依次取得数据元素,
算法思想:逆置链表初始为空,原表中结点从原链表中依次“删除”,再逐个插入逆置链表的表头(即“头插”到逆置链表中),使它成为逆置链表的“新”的第一个结点,如此循环,直至原链表为空;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 LNode *Inverse (LNode *L) { LNode *p, *q; p = L->next; L->next = NULL ; while (p != NULL ){ q = p; p = p->next; q->next = L->next; L->next = q; } return L; }
1.3.2双链表
单链表只包含指向后继节点的指针,找前驱结点复杂
而双链表是在单链表基础上再增加一个指针域
双链表中节点类型的描述:
1 2 3 4 typedef struct DNode { ElemType data; struct DNode *prior, *next; }DNode, *DLinklist;
DNode中的D:double
prior是指向结点的前驱结点
1.双链表的初始化(带头结点) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 typedef struct DNode { ElemType data; struct DNode *prior, *next; }DNode, *DLinklist; bool InitDLinkList (Dlinklist &L) { L = (DNode *)malloc (sizeof (DNode)); if (L==NULL ) return false ; L->prior = NULL ; L->next = NULL ; return true ; } void testDLinkList () { DLinklist L; InitDLinkList (L); } bool Empty (DLinklist L) { if (L->next == NULL ) return true ; else return false ; }
2.双链表的插入操作
后插操作 InsertNextDNode(p, s): 在p结点后插入s结点
1 2 3 4 5 6 7 8 9 10 11 12 bool InsertNextDNode (DNode *p, DNode *s) { if (p==NULL || s==NULL ) return false ; s->next = p->next; if (p->next != NULL ) p->next->prior = s; s->prior = p; p->next = s; return true ; }
如果没有后继节点,就不需要修改后继节点的前向指针
==均转化为后插操作==
3.双链表的删除操作 删除p节点的后继节点 q
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 bool DeletNextDNode (DNode *p) { if (p==NULL ) return false ; DNode *q =p->next; if (q==NULL ) return false ; p->next = q->next; if (q->next != NULL ) q->next->prior=p; free (q); return true ; } bool DestoryList (DLinklist &L) { while (L->next != NULL ){ DeletNextDNode (L); free (L); L=NULL ; } }
4.双链表的遍历操作 前向遍历
1 2 3 4 while (p!=NULL ){ p = p->prior; }
后向遍历
1 2 3 4 while (p!=NULL ){ p = p->next; }
注意:双链表不可随机存取,按位查找 和按值查找 操作都只能用遍历的方式实现,时间复杂度为O(n)
1.3.3循环链表 1.循环单链表 最后一个结点的指针不是NULL,而是指向头结点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 typedef struct LNode { ElemType data; struct LNode *next; }DNode, *Linklist; /初始化一个循环单链表 bool InitList (LinkList &L) { L = (LNode *)malloc (sizeof (LNode)); if (L==NULL ) return false ; L->next = L; return true ; } bool Empty (LinkList L) { if (L->next == L) return true ; else return false ; } bool isTail (LinkList L, LNode *p) { if (p->next == L) return true ; else return false ; }
单链表和循环单链表的比较: 单链表: 从一个结点出发只能找到该结点后续的各个结点;对链表的操作大多都在头部或者尾部;设立头指针,从头结点找到尾部的时间复杂度=O(n),即对表尾进行操作需要O(n)的时间复杂度;循环单链表: 从一个结点出发,可以找到其他任何一个结点;设立尾指针,从尾部找到头部的时间复杂度为O(1),即对表头和表尾进行操作都只需要O(1)的时间复杂度;
==优点:==从表中任一节点出发均可找到表中其他结点。
2.循环双链表 表头结点的prior指向表尾结点,表尾结点的next指向头结点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 typedef struct DNode { ElemType data; struct DNode *prior, *next; }DNode, *DLinklist; bool InitDLinkList (DLinklist &L) { L = (DNode *) malloc (sizeof (DNode)); if (L==NULL ) return false ; L->prior = L; L->next = L; } void testDLinkList () { DLinklist L; InitDLinkList (L); } bool Empty (DLinklist L) { if (L->next == L) return true ; else return false ; } bool isTail (DLinklist L, DNode *p) { if (p->next == L) return true ; else return false ; }
双链表的插入(循环双链表):
1 2 3 4 5 6 bool InsertNextDNode (DNode *p, DNode *s) { s->next = p->next; p->next->prior = s; s->prior = p; p->next = s; }
双链表的删除
1 2 3 4 p->next = q->next; q->next->prior = p; free (q);
双向循环链表:
和单链的循环表类似,双向链表也可以有循环表,让头结点的前驱指针指向链表的最后一个结点,让最后一个结点的后继指针指向头结点。
结构定义:
1 2 3 4 5 typedef struct DuLNode { Elemtype data; struct DulNode *prior ,*next ; } DuLNode,*DuLinkList;
1.3.4静态链表 1.定义:
单链表:各个结点散落在内存中的各个角落,每个结点有指向下一个节点的指针(下一个结点在内存中的地址);
静态链表:用数组的方式来描述线性表的链式存储结构: 分配一整片连续的内存空间,各个结点集中安置,包括了——数据元素and下一个结点的数组下标(游标)
其中数组下标为0的结点充当”头结点”
游标为-1表示已经到达表尾
若每个数据元素为4B,每个游标为4B,则每个结点共8B;假设起始地址为addr,则数据下标为2的存放地址为:addr+8*2
注意: 数组下标——物理顺序,位序——逻辑顺序;
优点:增、删操作不需要大量移动元素;
缺点:不能随机存取,只能从头结点开始依次往后查找,容量固定不变!
2.静态链表用代码表示: 1 2 3 4 5 6 7 8 9 10 11 12 #define MaxSize 10 struct Node { ElemType data; int next; }; void testSLinkList () { struct Node a[MaxSize]; }
也可以这样:
1 2 3 4 5 6 7 8 9 10 #define MaxSize 10 typedef struct { ELemType data; int next; }SLinkList[MaxSize]; void testSLinkList () { SLinkList a; }
也等同于:
1 2 3 4 5 6 7 8 #define MaxSize 10 struct Node { ElemType data; int next; }; typedef struct Node SLinkList[MaxSize];
注意:SLinkList a 强调a是静态链表;struct Node a 强调a是一个Node型数组;
3.静态链表基本操作的实现
初始化静态链表:把a[0]的next设为-1(相当于头结点先指向NULL)
查找某个位序(不是数组下标,位序是各个结点在逻辑上的顺序)的结点:从头结点出发挨个往后遍历结点,时间复杂度O=(n)
在位序为i上插入结点:① 找到一个空的结点,存入数据元素;② 从头结点出发找到位序为i-1的结点;③修改新结点的next;④ 修改i-1号结点的next;
删除某个结点:① 从头结点出发找到前驱结点;② 修改前驱节点的游标;③ 被删除节点next设为-2;
1.3.5顺序表和链表的比较 1.逻辑结构
2.存储结构
顺序表:顺序存储
优点:支持随机存取,存储密度高
缺点:大片连续空间分配不方便,改变容量不方便
链表:链式存储
优点:离散的小空间分配方便,改变容量方便
缺点:不可随机存取,存储密度低
3. 基本操作 - 创建
顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源;
静态分配:静态数组,容量不可改变
动态分配:动态数组,容量可以改变,但是需要移动大量元素,时间代价高(malloc(),free())
链表:只需要分配一个头结点或者只声明一个头指针
4. 基本操作 - 销毁
1 2 3 4 5 typedef struct { ElemType *data; int MaxSize; int length; }SeqList;
1 2 3 4 5 6 L.data = (ELemType *)malloc (sizeof (ElemType) *InitSize) free (L.data);
5.基本操作-增/删
顺序表:插入/删除元素要将后续元素后移/前移;时间复杂度=O(n),时间开销主要来自于移动元素;
链表:插入/删除元素只需要修改指针;时间复杂度=O(n),时间开销主要来自查找目标元素
6.基本操作-查
顺序表
按位查找:O(1)(直接可以找到地址)
按值查找:O(n),若表内元素有序,可在O(log2n)时间内找到(折半查找)
链表
1.3.6顺序、链式、静态、动态四种存储方式的比较
顺序存储的固有特点: 逻辑顺序与物理顺序一致,本质上是用数组存储线性表的各个元素(即随机存取);存储密度大,存储空间利用率高。
链式存储的固有特点: 元素之间的关系采用这些元素所在的节点的“指针”信息表示(插、删不需要移动节点)。
静态存储的固有特点: 在程序运行的过程中不要考虑追加内存的分配问题。
动态存储的固有特点: 可动态分配内存;有效的利用内存资源,使程序具有可扩展性。
1.3.7链表的逆置算法 思路:先将链表一个一个的断开,再将断开的链表插入到原来的队列中