1、为什么要使用链式存储结构?
因为我们前面讲的线性表的顺序存储结构,他是有缺点的。最大的缺点就是插入和删除时需要移动大量元素,这显然就需要耗费时间。要解决这个问题,我们就需要分析一下为什么当插入和删除时,就要移动大量元素,因为相邻两元素的存储位置也具有相邻关系,它们在内存中的位置也是挨着的,中间没有空隙,当然就无法快速介入,而删除之后。当中就会留出空隙,自然就需要弥补。问题就出在这里。
为了解决这个问题,自然而然的就出现了链式存储结构。
2、线性表链式存储结构的特点:
线性表的链式存储结构不考虑元素的存储位置,而是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的,这就意味着,这些数据元素可以存在内存未被占用的任意位置。
顺序存储结构:只需要存储数据元素信息。
链式存储结构:除了要存储数据元素信息之外,还要存储一个指示其直接后继元素的存储地址。
3、关键词:
数据域:存储数据元素信息的域。
指针域:存储直接后继位置的域。
指针或链:指针域中存储的信息。
结点(Node):指针域+数据域组成数据元素的存储映像。
头指针:链表中第一个结点的存储位置。
头节点:在单链表的第一个结点前附设一个结点,成为头结点。头结点的数据域不可以存储任何信息,可以存储线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。
4、单链表:
定义:n个结点链成一个链表,即为线性表的链式存储结构,因此此链表的每个结点中只包含一个指针域,所以叫做单链表。
PS:线性链表的最后一个结点指针为“空”,通常用NILL或“^”符号表示。
头节点:在单链表的第一个结点前附设一个结点,成为头结点。头结点的数据域不可以存储任何信息,可以存储线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。
5、头结点与头指针的异同
(1)头结点
-
- 头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(也可存放链表的长度)
- 有了头结点,对第一元素结点前插入和删除第一结点,其操作就统一了
- 头结点不一定是链表的必要素
(2)头指针
-
- 头指针式指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
- 头指针具有标识作用,所以常用头指针冠以链表的名字。
- 无论链表是否为空,头指针均不为空。头指针是了链表的必要元素。
6、线性表链式存储结构代码描述
若线性表尾空表,则头结点的指针域为“空”
没有头结点的单链表:
带有头结点的单链表:
线性表的单链表存储结构:
1 typedefstructNode{ 2 3 ElemTypedata; 4 structNode*next; 5 }Node;
7、单链表的读取「GetElem」
算法思路:
-
- 声明一个结点p指向链表的第一个结点,初始化j从1开始;
- 当 j
- 若到链表末尾p为空,则说明第i个元素不存在;
- 否则查找成功,返回结点p的数据。
实现代码:
1 /* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L) */ 2 /* 操作结果:用e返回L中第i个数据元素的值 */ 3 Status GetElem(LinkList L, int i, ElemType *e){ 4 5 int j = 1; 6 LinkList p = L->next; /* 声明一结点p,并让p指向L的第一个结点 */ 7 8 while (p && j < i) { /* p不为空并且计数器j还没有等于i时,循环继续 */ 9 p = p->next; /* 让p指向下一个结点 */ 10 ++ j; 11 } 12 13 if (!p || j > i) { 14 return Error; /* 第i个元素不存在 */ 15 } 16 17 *e = p->data; /* 取第i个元素的数据 */ 18 return OK; 19 }
算法复杂度分析:
算法复杂度取决于 i 的位置,当 i = 1 时,则不需要遍历,第一个就取出数据了,而当 i = n 时则遍历 n - 1 次才可以。因此最坏情况的 时间复杂度是O(n)。
注:链式存储的优势不在查找元素,在于插入和删除元素。
8、单链表的插入
单链表第 i 个数据插入结点的算法思路:
1、声明一个结点 p 指向链表第一个结点,初始化 j 从 1 开始;
2、当 j < i 时,就遍历链表,让 p 的指针向后移动,不断指向下一个结点,j 累加1;
3、若找到链表末尾 p 为空,则说明第 i 个元素不存在;
4、否则查找成功,在系统中生成一个空结点s;
5、将数据元素 e 赋值给 s -> data ;
6、单链表的插入标准语句 s->next = p -> next , p -> next = s ;
7、返回成功。
代码实现:
1 /* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L), */ 2 /* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */ 3 Status ListInsert(LinkList *L, int i, ElemType e){ 4 5 int j = 1; 6 LinkList p = *L,s; 7 while (p && j/* 寻找第i个结点,也就是说当循环结束的时候,已经找到了要插入位置的前一个位置*/ 8 p = p->next; 9 ++ j; 10 } 11 if(!p || j > i){ /* 第i个元素不存在 */ 12 return Error; 13 } 14 s = (LinkList)malloc(sizeof(Node)); /* 生成新结点(C语言标准函数) */ 15 s->data = e; 16 s->next = p->next; /* 将p的后继结点赋值给s的后继 */ 17 p->next = s; /* 将s赋值给p的后继 */ 18 return OK; 19 }
malloc函数的作用就是生成一个新的结点,其类型与Node是一样的,其实质就是在内存中找到了一小块空地用来存放e数据s结点。
9、单链表的删除
算法思路:
1、生命一个结点p指向链表的第一个结点,初始化 j 从 1 开始;
2、当 j < i 时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j 累加 1;
3、若遍历到链表的末尾p为空,则说明第i个元素不存在;
4、否则查找成功将欲删除的结点 p -> next 赋值给 q ;
5、单链表的删除标准语句:p -> next = q -> next ;
6、将 q 中结点中的数据赋值给e,作为返回;
7、释放 q 结点;
8、返回成功。
实现代码:
1 Status ListDelete(LinkList *L, int i, ElemType *e){ 2 3 int j = 1; 4 LinkList p,q; 5 p = *L; 6 while (p->next && j < i) { 7 p = p->next; 8 ++ j; 9 } 10 if(!(p->next) || j > i){ 11 return Error; 12 } 13 14 q = p->next; 15 p->next = q->next; 16 *e = q->data; 17 free(q); 18 return OK; 19 } 20 注:free函数的作用是让系统回收一个Node结点,释放内存。
10、单链表的整表创建
创建单链表的过程就是一个动态生成链表的过程。即从“空表”的初始态起,依次创建各元素结点,并逐个插入链表。
- 头插法
单链表整表创建的算法思路:
1、生命一个结点p和计数器i;
2、初始化一空链表L;
3、让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
4、循环:
(1)生成一个新结点赋值给p;
(2)随机生成一数字赋值给p的数据域p->data ;
(3)将p插入到头结点与前一结点之间。
代码实现:
1 /* 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */ 2 Status CreateListHead(LinkList *L,int n){ 3 LinkList p; 4 int i; 5 srand(time(0)); /* 初始化随机数种子 */ 6 *L = (LinkList)malloc(sizeof(Node)); 7 (*L)->next = NULL; /* 先建立一个带头结点的单链表 */ 8 for(i = 0; i < n; i ++){ 9 p = (LinkList)malloc(sizeof(Node)); /* 生成新结点 */ 10 p->data = rand()%100+1; /* 随机生成100以内的数字 */ 11 p->next = (*L)->next; 12 (*L)->next = p; /* 插入到表头 */ 13 } 14 return OK; 15 }
- 尾插法
实现代码:
1 /* 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */ 2 void CreateListTail(LinkList *L, int n) 3 { 4 LinkList p,r; 5 int i; 6 srand(time(0)); /* 初始化随机数种子 */ 7 *L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */ 8 r=*L; /* r为指向尾部的结点 */ 9 for (i=0; i) 10 { 11 p = (Node *)malloc(sizeof(Node)); /* 生成新结点 */ 12 p->data = rand()%100+1; /* 随机生成100以内的数字 */ 13 r->next=p; /* 将表尾终端结点的指针指向新结点 */ 14 r = p; /* 将当前的新结点定义为表尾终端结点 */ 15 } 16 r->next = NULL; /* 表示当前链表结束 */ 17 }
11、单链表的整表删除
算法思路:
1、声明一个结点p和q;
2、将第一个结点赋给p;
3、循环:
(1)将下一结点赋给q;
(2)释放p;
(3)将q赋值给p;
代码实现:
1 /* 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */ 2 Status CreateListHead(LinkList *L,int n){ 3 LinkList p; 4 int i; 5 srand(time(0)); /* 初始化随机数种子 */ 6 *L = (LinkList)malloc(sizeof(Node)); 7 (*L)->next = NULL; /* 先建立一个带头结点的单链表 */ 8 for(i = 0; i < n; i ++){ 9 p = (LinkList)malloc(sizeof(Node)); /* 生成新结点 */ 10 p->data = rand()%100+1; /* 随机生成100以内的数字 */ 11 p->next = (*L)->next; 12 (*L)->next = p; /* 插入到表头 */ 13 } 14 return OK; 15 }
12、单链表结构与顺序存储结构优缺点
(1)存储分配方式
-
- 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
- 单链表采用链式存储结构,用一组任意的存储单元依次存放线性表的元素
(2)时间性能
-
- 查找
- 顺序存储结构O(1)
- 单链表O(n)
- 插入和删除
- 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
- 单链表在找出某位置的指针后,插入和删除时间仅为O(1)
- 空间性能
- 顺序存储结构需要预分配存储空间,分大了,浪费,分小了容易溢出。
- 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制。
- 查找
总结:
- 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若插入和删除频繁时,宜采用单链表结构。
- 当线性表的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样不用考虑存储空间的大小问题。而如果事先知道了线性表的长度时,则使用顺序结构效率会比价高。
测试源码
1 // 2 // main.cpp 3 // 线性表的链式存储结构--单链表 4 // 5 // Created by Mr.G on 2018/10/17. 6 // Copyright © 2018年 Mr.G. All rights reserved. 7 // 8 9 #include "stdio.h" 10 #include "stdlib.h" 11 #include "string.h" 12 #include "ctype.h" 13 14 #include "math.h" 15 #include "time.h" 16 17 #define OK 1 18 #define Error 0 19 #define TRUE 1 20 #define FALSE 0 21 22 #define MAXSIZE 23 24 typedef int Status; 25 typedef int ElemType; 26 typedef struct Node{ 27 28 ElemType data; 29 struct Node *next; 30 }Node; 31 typedef struct Node *LinkList; /* 定义LinkList */ 32 33 Status visit(ElemType c){ 34 printf("%d\t",c); 35 return OK; 36 } 37 38 /* 初始化顺序线性表 */ 39 Status InitList(LinkList *L){ 40 41 *L = (LinkList)malloc(sizeof(Node)); /* 产生头结点,并使L指向此头结点*/ 42 if(!(*L)){ /* 存储分配失败*/ 43 return Error; 44 } 45 (*L)->next = NULL; /* 指针域为空*/ 46 47 return OK; 48 } 49 50 int ListLength(LinkList L){ 51 int i = 0; 52 LinkList p = L->next; 53 while (p) { 54 i++; 55 p = p->next; 56 } 57 return i; 58 } 59 60 /* 初始条件:顺序线性表L已存在 */ 61 /* 操作结果:依次对L的每个数据元素输出 */ 62 Status ListTraverse(LinkList L){ 63 LinkList p = L->next; 64 while (p) { 65 visit(p->data); 66 p = p->next; 67 } 68 printf("\n"); 69 return OK; 70 } 71 72 /* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L), */ 73 /* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */ 74 Status ListInsert(LinkList *L, int i, ElemType e){ 75 76 int j = 1; 77 LinkList p = *L,s; 78 while (p && j/* 寻找第i个结点,也就是说当循环结束的时候,已经找到了要插入位置的前一个位置*/ 79 p = p->next; 80 ++ j; 81 } 82 if(!p || j > i){ /* 第i个元素不存在 */ 83 return Error; 84 } 85 s = (LinkList)malloc(sizeof(Node)); /* 生成新结点(C语言标准函数) */ 86 s->data = e; 87 s->next = p->next; /* 将p的后继结点赋值给s的后继 */ 88 p->next = s; /* 将s赋值给p的后继 */ 89 return OK; 90 } 91 92 /* 初始条件:顺序线性表L已存在。操作结果:若L为空表,则返回TRUE,否则返回FALSE */ 93 Status ListEmpty(LinkList L){ 94 95 if(L->next) 96 return FALSE; 97 else 98 return TRUE; 99 } 100 101 /* 初始条件:顺序线性表L已存在。操作结果:将L重置为空表 */ 102 Status ClearList(LinkList *L){ 103 104 LinkList p,q; 105 p = (*L)->next; /* p指向第一个结点 */ 106 while (p) { 107 q = p->next; 108 free(p); 109 p = q; 110 } 111 (*L)->next = NULL; 112 return OK; 113 } 114 115 /* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L) */ 116 /* 操作结果:用e返回L中第i个数据元素的值 */ 117 Status GetElem(LinkList L, int i, ElemType *e){ 118 119 int j = 1; 120 LinkList p = L->next; /* 声明一结点p,并让p指向L的第一个结点 */ 121 122 while (p && j < i) { /* p不为空并且计数器j还没有等于i时,循环继续 */ 123 p = p->next; /* 让p指向下一个结点 */ 124 ++ j; 125 } 126 127 if (!p || j > i) { 128 return Error; /* 第i个元素不存在 */ 129 } 130 131 *e = p->data; /* 取第i个元素的数据 */ 132 return OK; 133 } 134 135 int LocateElem(LinkList L,ElemType e){ 136 int i = 0; 137 LinkList p = L->next; 138 while (p) { 139 i ++; 140 if(p->data == e){ 141 return i; 142 } 143 p = p->next; 144 } 145 146 return 0; 147 } 148 149 Status ListDelete(LinkList *L, int i, ElemType *e){ 150 151 int j = 1; 152 LinkList p,q; 153 p = *L; 154 while (p->next && j < i) { 155 p = p->next; 156 ++ j; 157 } 158 if(!(p->next) || j > i){ 159 return Error; 160 } 161 162 q = p->next; 163 p->next = q->next; 164 *e = q->data; 165 free(q); return OK; 166 } 167 168 /* 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */ 169 Status CreateListHead(LinkList *L,int n){ 170 LinkList p; 171 int i; 172 srand(time(0)); /* 初始化随机数种子 */ 173 *L = (LinkList)malloc(sizeof(Node)); 174 (*L)->next = NULL; /* 先建立一个带头结点的单链表 */ 175 for(i = 0; i < n; i ++){ 176 p = (LinkList)malloc(sizeof(Node)); /* 生成新结点 */ 177 p->data = rand()%100+1; /* 随机生成100以内的数字 */ 178 p->next = (*L)->next; 179 (*L)->next = p; /* 插入到表头 */ 180 } 181 return OK; 182 } 183 184 /* 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */ 185 void CreateListTail(LinkList *L, int n) 186 { 187 LinkList p,r; 188 int i; 189 srand(time(0)); /* 初始化随机数种子 */ 190 *L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */ 191 r=*L; /* r为指向尾部的结点 */ 192 for (i=0; i) 193 { 194 p = (Node *)malloc(sizeof(Node)); /* 生成新结点 */ 195 p->data = rand()%100+1; /* 随机生成100以内的数字 */ 196 r->next=p; /* 将表尾终端结点的指针指向新结点 */ 197 r = p; /* 将当前的新结点定义为表尾终端结点 */ 198 } 199 r->next = NULL; /* 表示当前链表结束 */ 200 } 201 202 int main() { 203 204 LinkList L; 205 ElemType e; 206 Status i; 207 int j,k; 208 i = InitList(&L); 209 printf("初始化L后:ListLength(L)=%d\n",ListLength(L)); 210 for(int j = 1; j <=5; j ++){ 211 i = ListInsert(&L, 1, j); 212 } 213 printf("在L的表头依次插入1~5后:L.data="); 214 ListTraverse(L); 215 216 printf("ListLength(L)=%d \n",ListLength(L)); 217 i = ListEmpty(L); 218 printf("L是否空:i=%d(1:是 0:否)\n",i); 219 220 i = ClearList(&L); 221 printf("清空L后:ListLength(L)=%d\n",ListLength(L)); 222 i = ListEmpty(L); 223 printf("L是否空:i=%d(1:是 0:否)\n",i); 224 225 for (int j = 1; j <= 10; j ++) { 226 ListInsert(&L, j, j); 227 } 228 printf("在L的表尾依次插入1~10后:L.data="); 229 ListTraverse(L); 230 printf("ListLength(L)=%d \n",ListLength(L)); 231 232 ListInsert(&L,1,0); 233 printf("在L的表头插入0后:L.data="); 234 ListTraverse(L); 235 printf("ListLength(L)=%d \n",ListLength(L)); 236 237 GetElem(L,5,&e); 238 printf("第5个元素的值为:%d\n",e); 239 240 for(int j = 3; j <= 4; j ++){ 241 k = LocateElem(L, j); 242 if(k) 243 printf("第%d个元素的值为%d\n",k,j); 244 else 245 printf("没有值为%d的元素\n",j); 246 } 247 248 k=ListLength(L); /* k为表长 */ 249 for (j = k + 1; j >= k; j --) { 250 i = ListDelete(&L, j, &e); 251 if(i == Error){ 252 printf("删除第%d个数据失败\n",j); 253 }else{ 254 printf("删除第%d个的元素值为:%d\n",j,e); 255 } 256 } 257 printf("依次输出L的元素:"); 258 ListTraverse(L); 259 260 j=5; 261 ListDelete(&L,j,&e); /* 删除第5个数据 */ 262 printf("删除第%d个的元素值为:%d\n",j,e); 263 264 printf("依次输出L的元素:"); 265 ListTraverse(L); 266 267 i=ClearList(&L); 268 printf("\n清空L后:ListLength(L)=%d\n",ListLength(L)); 269 CreateListHead(&L,20); 270 printf("整体创建L的元素(头插法):"); 271 ListTraverse(L); 272 273 i=ClearList(&L); 274 printf("\n删除L后:ListLength(L)=%d\n",ListLength(L)); 275 CreateListTail(&L,20); 276 printf("整体创建L的元素(尾插法):"); 277 ListTraverse(L); 278 279 return 0; 280 }