数据结构(二):线性表的链式存储结构

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 }
 
 
 
 
 
    

转载于:https://www.cnblogs.com/xiaovw/articles/9969791.html

你可能感兴趣的:(数据结构与算法,c/c++)