线性表有两种:一种是顺序存储的叫顺序表,上节已经说过了,另外一种是链式存储的叫链表,本节说的是单链表,即单向链表(每一个节点中只包含一个指针域)。node
本节知识点:
1.链表的好处:对于动态链表,能够对未知数据量的数据进行存储。插入和删除比顺序表方便的多,不用大量移动。
链表的缺点:除了数据信息,还需对额外的链表信息进行分配内存,占用了额外的空间。访问指定数据的元素须要顺序访问以前的元素。
2.链表的基本概念:
链表头(表头节点):链表中的第一个节点,包含指向第一个数据元素的指针以及链表自身的一些信息(即链表长度length)
数据节点:链表中的表明数据元素的节点,包含指向下一个数据元素的指针和数据元素的信息
尾节点:链表中的最后一个数据节点,其下一元素指针为空,表示无后继
3.
对于本节的可复用单链表的设计想法是这样的:
a. 可复用的顺序表中保存的是各个数据的地址,因此我最初想到的是在链表元素中也保存各个数据的地址:
使用这样的结构,add是链表中保存的数据,其实就是想复用保存的各类类型的地址,add是一个unsigned int型,用来保存各类数据类型的地址,next是链表结构,用来指向链表元素的下一个链表元素的。
b.可是这样的结构有一个问题,就是从使用的总空间(链表结构的空间+add中保存数据的空间)角度来看,add就是一个浪费空间的变量。由于在add中保存地址,为何不强制类型成next的类型(此时next应该是链表第一个结构的类型),直接使用这个地址把各类你想要存储的结构赋值给next,这样存储的各个结构就变成了,如图。
c.可是把全部的类型都转换成链表第一个元素的指针类型 再赋值给next 显得程序很不规整,因此最好直接给链表一个结构,把这些结构类型都统一强制类型转换成这个链表的类型,以下:
- typedef struct Str_LinkList LinkListNode;
- struct Str_LinkList
- {
- LinkListNode* next;
- };
把什么链表头啊,链表元素啊,想要链接进入这个链表的各类结构都强制类型成 LinkListNode* 可是要保证每一个结构中都有LinkListNode* next 成员。
d.最后一点,就有点接受不了,也是为了代码的整洁,提升可读性,使链表结构都规整到LinkListNode这个结构中去,便于对链表进行管理,好比说双向链表的前驱和后继。把每一个类型中的LinkListNode* next 成员 变成LinkListNode node。
这里面有一个很好的c语言技巧,就是这个LinkListNode node必需要放在每一个结构中(如 str)的第一个元素位置,即node的地址就是结构体str的地址,由于只有这样了,在把str强制类型转换成 n=(LinkListNode* )str的时候,访问n->next才是访问str.node->next的值,由于二者地址相同,切记必定要放到第一个元素的位置!!!
4.对于链表这个数据结构,必定要注意一个问题,也是这节我犯的一个很难发现的错误:
就是已经在链表中的元素,千万不要再一次往链表中进行插入,由于这样会致使从它插入的地方开始链表的后继就开始混乱了,把整个链表彻底弄乱,出现你想不到的问题。
本节代码:
本节实现的是一个能够复用的单链表:
LinkList.c:
- #ifndef __LinkList_H__
- #define __LinkList_H__
-
- typedef void LinkList;
- typedef struct Str_LinkList LinkListNode;
- struct Str_LinkList
- {
- LinkListNode* next;
- };
-
- LinkList* Creat_LinkListHead(void);
-
- int Destroy_LinkListHead(LinkList* head);
-
- int Get_Length(LinkList* head);
-
- int Clean_LinkListHead(LinkList* head);
-
- int Add_LinkList(LinkList* head, LinkListNode* Node, int pos);
-
- LinkListNode* Get_LinkListNode(LinkList* head, int pos);
-
- LinkListNode* Del_LinkListNode(LinkList* head, int pos);
-
- #endif
main.c:
- #include <stdio.h>
- #include <stdlib.h>
- #include <malloc.h>
- #include <string.h>
- #include "LinkList.h"
-
- typedef struct student
- {
-
- LinkListNode node;
- int num;
- char name[30];
- }str;
- int main(int argc, char *argv[])
- {
- str str1,str2,str3,str4,str5,str6,*strp;
- int i=0;
- LinkList* list_head;
- list_head = Creat_LinkListHead();
-
- str1.num = 1;
- strcpy(str1.name,"haohao");
-
- str2.num = 2;
- strcpy(str2.name,"ququ");
-
- str3.num = 3;
- strcpy(str3.name,"popo");
-
- str4.num = 4;
- strcpy(str4.name,"wowo");
-
- str5.num = 5;
- strcpy(str5.name,"tiantian");
-
- str6.num = 6;
- strcpy(str6.name,"cheche");
-
- Add_LinkList(list_head, (LinkListNode*)&str1, 0);
- Add_LinkList(list_head, (LinkListNode*)&str2, 0);
- Add_LinkList(list_head, (LinkListNode*)&str3, 0);
- Add_LinkList(list_head, (LinkListNode*)&str4, 0);
- Add_LinkList(list_head, (LinkListNode*)&str5, 0);
- strp = (str*)Del_LinkListNode(list_head, 5);
- printf("%d\n",strp->num);
- printf("%s\n",strp->name);
- printf("\n");
- for(i=1; i<= Get_Length(list_head); i++)
- {
- strp = (str*)Get_LinkListNode(list_head, i);
- printf("%d\n",strp->num);
- printf("%s\n",strp->name);
- }
- printf("\n");
- Add_LinkList(list_head, (LinkListNode*)&str6, 3);
- for(i=1; i<= Get_Length(list_head); i++)
- {
- strp = (str*)Get_LinkListNode(list_head, i);
- printf("%d\n",strp->num);
- printf("%s\n",strp->name);
- }
-
-
- Clean_LinkListHead(list_head);
- Destroy_LinkListHead(list_head);
- return 0;
- }
课后练习:
1.对于上节顺序表中的unsigned int型保存数据地址,可不可用void*
这个问题已经获得了唐老师的解答,其实使用void* 最好了,由于使用unsigned int 仅仅可以在32位机上面运行成功,在64位机上运行就会出错的!!!
2.对于有头链表和无头链表的区别
所谓有头链表,就是有头结点的链表,头结点是一个链表元素,但不存放数据。无头链表就是没有头结点的链表。
相比之下有头链表比无头链表,方便不少,优势也不少。
无头链表就是在谭浩强老师的c语言书中的那个链表。就是没有头结点,只有一个指针指向链表第一个元素。这个指针被叫作链表头。 我的建议使用有头链表!!!
3.对顺序表和单链表添加一个反转操做
a.对于顺序表 其实就是在不断的交换
b.对于链表 我以为仍是使用双向链表吧,解决这个问题就方便了~~~
本节知识点:
1.静态链表究竟是什么:链表就是链式存储的线性表,可是它分为
动态和
静态两种,所谓动态就是长度不固定,能够根据状况自行扩展大小的,静态链表就是长度大小固定的,链式存储的线性表。
2.本节的静态链表和顺序表很像(其实和数组也很像),
准确的来讲就是利用顺序表实现的,只是这个顺序表,不是顺序排列的,是经过一个next变量,链接到下一个变量的。
如图:
3.唐老师说静态链表是在一些没有指针的语言中使用的,来实现链表的功能,可是我以为链表的最大优点就在于它的伸缩,用多少开辟多少。可是静态链表就偏偏失去了这个优点。依我看,
学习静态链表的目的是学习它这种相似内存管理的算法思想。
4.静态链表中值得学习的思想:就是在初始化链表的时候,把因此空间都标记成为可用-1,每次插入数据的时候,都在标记为可用的空间内挑取,再把-1改为next。当删除变量的时候在把next改为-1,标记为空间可用。
5.其实仔细想一想 看看,静态链表只是一个思想,为何这么说,首先在获取index的时候,你是顺序获取的,这致使你的next也是连续的,因此他其实就变成了一个顺序表。在这里我想到了一个唐老师的问题,为何node[0]就能够看成头节点,还要再定义一个head变量。唐老师的解答是:顺序得到index的时候每次都要遍历太浪费时间了,因此最好应该在同一块空间再定义一个链表,来保存这些空闲空间,而后这样就须要两个链表的头节点了,因此须要一个head。而后让node[0]一会是空闲链表的链表头节点,一会是真实保存数据的链表的头节点。当插入的时候,只须要在那个空闲链表取空间就能够了,提升了算法的效率。
PS1:对于node[0]我真的想说,其实让node[0]看成头节点的使用真的很方便,比head方便不少,仅仅是我的体会。
PS2:对于两个链表的那个算法,我以为若是仍是顺序在链表中得到index,依然没有解决这个index是有顺序的且顺序是固定的问题。这里的顺序是指的是那个空闲链表的顺序。因此说这仅仅是一个思想。
6.
本节最重要的知识点也是最大的难点:对于柔性数组的描述。
对于柔性数组的结构以下:
- typedef struct _tag_StaticList
- {
- int capacity;
- StaticListNode head;
- StaticListNode node[];
- }StaticList;
而后:给柔性数组开辟空间
- StaticList* ret = NULL;
- ret = (StaticList*)malloc( sizeof(StaticList)*1 + sizeof(StaticListNode)*(capacity+1) );
其实柔性数组就是以数组的方式访问内存。对于 StaticList ret这个结构体的大小是不包括StaticLIstNode node[]的,StaticLIstNode node[]是没有大小的,StaticLIstNode node[0]访问的内存是StaticList ret这个结构体后面的第一个内存,StaticLIstNode node[1]访问的内存是StaticList ret这个结构体后面的第二个内存等等。
PS:StaticLIstNode node[]这个结构究竟是个什么结构,很差说,不是数组,也不是指针。就把它看成为了柔性数组而产生的结构吧!!!
本节代码:
StaticList.c:
StaticList.h:
- #ifndef __STATICLIST_H__
- #define __STATICLIST_H__
-
- typedef void SList;
- typedef void SListNode;
-
- SList* Creat_StaticList(int capacity);
- void Destroy_StaticList(SList* Static_List);
- int Get_Lenth(SList* List);
- int Get_Capacity(SList* List);
- int Clear_StaticList(SList* List);
- int Add_StaticList(SList* List, SListNode* Node, int pos);
- SListNode* Get_StaticListNode(SList* List, int pos);
- SListNode* Del_StaticListNode(SList* List, int pos);
-
- #endif
main.c:
- #include <stdio.h>
- #include <malloc.h>
- #include <string.h>
- #include "StaticList.h"
-
- int main()
- {
- SList* list = Creat_StaticList(10);
- int *f = 0;
- int i = 0;
- int a = 1;
- int b = 2;
- int c = 3;
- int d = 4;
- int e = 5;
-
- Add_StaticList(list, &a, 0);
- Add_StaticList(list, &b, 0);
- Add_StaticList(list, &c, 0);
- Add_StaticList(list, &d, 0);
-
- for(i=1; i<=Get_Lenth(list); i++)
- {
- f=(int* )Get_StaticListNode(list, i);
- printf("%d\n",*f);
- }
-
- Add_StaticList(list, &e, 2);
- printf("\n");
- for(i=1; i<=Get_Lenth(list); i++)
- {
- f=(int* )Get_StaticListNode(list, i);
- printf("%d\n",*f);
- }
-
- printf("\n");
- f=(int* )Del_StaticListNode(list, 4);
- printf("del %d\n",*f);
- printf("\n");
- for(i=1; i<=Get_Lenth(list); i++)
- {
- f=(int* )Get_StaticListNode(list, i);
- printf("%d\n",*f);
- }
- Destroy_StaticList(list);
- return 0;
- }
本节知识点:
1.为何选择循环链表:由于有不少生活中结构是循环的,是单链表解决不了的,好比星期、月份、24小时,对于这些循环的数据,循环链表就体现出它的优点了。算法
2.循环链表的结构:数组

循环链表就是从头结点后面开始,尾节点的next再也不是NULL了,而是头结点后面的第一个链表元素,如上图。安全
3.如何建立一个循环链表:数据结构
步骤一:dom

步骤二:ide

不管是头插法,仍是尾插法都没有关系,均可以建立完成这个循环链表。函数
4.如何将一个单向链表改写成一个循环链表:学习
第一步 (改写插入函数):
a.把插入位置pos的容许范围改为0~~~无穷大
- ret=( NULL != node) && ( NULL != Node) && (pos >= 0);
b.把两种方式的头插法状况加入程序,第一种是pos值为0和1的状况,如图:

这种状况分为两部:先把node插入到head和第一个元素直接,而后再把链表尾指向node元素(node表示插入元素)。
代码以下:
- if(node == (CircleListNode* )head)
- {
- Last =(CircleListNode* )Get_CircleListNode(lhead, lhead->length);
- Last->next = Node;
- }
头插法的第二种状况,是循环链表,循环了一圈回来了,与第一种不一样的是此时插入的相对位置和第一种的相对位置不同。(其实这种方法跟普通插入是同样的) 如图:

第二步 (改写删除函数):
a.也是把pos值的取值范围改为0 到 无穷大,可是同时记得判断length要大于0 ,要保证链表中有数据,否则删什么呀~~~~
- if(( NULL != lhead) && (pos > 0) && (lhead->length>0))
b.对于删除第一个元素有两种状况 这里是难点:首先要在删除链表元素的 前面 判断是否要删除第一个元素(此时的状况是pos为1的状况),而后删除链表元素,再判断是不是删除第一个元素的第二种状况(链表循环一圈后,到达链表第一个元素,此时元素的前一个链表再也不是head头结点了)。如图:

代码以下:
- if(node == (CircleListNode* )head)
- {
- Last =(CircleListNode* )Get_CircleListNode(lhead, lhead->length);
- }
-
- ret = node->next;
- node->next = ret->next;
-
- if((first == ret) &&(NULL == Last))
- {
- Last =(CircleListNode* )Get_CircleListNode(lhead, lhead->length);
- }
-
- if( Last != NULL )
- {
- Last->next = ret->next;
- lhead->head.next = ret->next;
- }
图中红笔的代码是:
- ret = node->next;
- node->next = ret->next;
图中蓝笔的代码是:
- if( Last != NULL )
- {
- Last->next = ret->next;
- lhead->head.next = ret->next;
- }
c.当length为0的是,即链表长度为0的时候,记得给头结点的next赋值为NULL
第三步 (改写得到链表元素函数)
a.记得把pos给成 0 到 无穷大,而后判断length链表长度是否为0 ,若是为0 就不能获取。
5.游标的引入:
在循环链表中通常能够定义一个游标,对于这样一个封装好的可复用循环链表,定义一个游标是十分方便的。例如:若是想依次得到链表中的每个元素,利用get函数,太太低效了O(n2),想一想利用这样一个游标去遍历的话,复杂度仅仅是O(n)。还有就是在循环链表中,游标能够在链表中进行转圈,例如:能够解决约瑟夫环问题。

6.指定删除链表中某一个元素的函数CircleListNode* CircleList_Del(CircleList* head,CircleListNode* node),其实也不是很高效,得到了当前游标的值的时候,再去调用CircleList_Del函数,这个轮询函数得到了pos,再去调用Del_CircleListNode而后又遍历了一边,把复杂的搞到了O(n2)。其实彻底能够在找到pos的时候直接删除掉这个链表元素,这样的复杂度是O(n)。
7.我还以为得到当前游标得值的函数CircleList_Slider的返回值有些问题,我以为若是返回的是当前游标的上一个链表元素的值会更好,由于这个是一个单向链表,若是获得了上一个链表元素的值,就能够经过游标实现,删除啊,插入啊等高效的操做了。
本节代码:
CricleList.c:
CircleList.h:
- #ifndef __CircleList_H__
- #define __CircleList_H__
-
- typedef void CircleList;
- typedef struct Str_CircleList CircleListNode;
- struct Str_CircleList
- {
- CircleListNode* next;
- };
-
- CircleList* Creat_CircleListHead(void);
-
- int Destroy_CircleListHead(CircleList* head);
-
- int Get_Length(CircleList* head);
-
- int Clean_CircleListHead(CircleList* head);
-
- int Add_CircleList(CircleList* head, CircleListNode* Node, int pos);
-
- CircleListNode* Get_CircleListNode(CircleList* head, int pos);
-
- CircleListNode* Del_CircleListNode(CircleList* head, int pos);
-
- CircleListNode* CircleList_Del(CircleList* head,CircleListNode* node);
-
- CircleListNode* CircleList_Next(CircleList* head);
-
- CircleListNode* CircleList_Reset(CircleList* head);
-
- CircleListNode* CircleList_Slider(CircleList* head);
-
- #endif
main.c:
- #include <stdio.h>
- #include <stdlib.h>
- #include "CircleList.h"
-
- typedef struct _tag_str
- {
- CircleListNode head;
- int i;
- }str;
- int main(int argc, char *argv[])
- {
- str str1,str2,str3,str4,str5,str6;
- str *strp;
- int i=0;
- str1.i=1;
- str2.i=2;
- str3.i=3;
- str4.i=4;
- str5.i=5;
- str6.i=6;
- CircleList* head;
- head = Creat_CircleListHead();
-
- Add_CircleList(head, (CircleListNode*)&str1, 0);
- Add_CircleList(head, (CircleListNode*)&str2, 0);
- Add_CircleList(head, (CircleListNode*)&str3, 0);
- Add_CircleList(head, (CircleListNode*)&str4, 0);
- Add_CircleList(head, (CircleListNode*)&str5, 5);
-
- for(i=1; i<=2*Get_Length(head); i++)
- {
- strp = (str* )Get_CircleListNode(head, i);
- printf("%d\n",strp->i);
- }
- printf("\n");
-
- printf("%d\n",Get_Length(head));
- strp = (str* )Del_CircleListNode(head, 6);
- printf("%d\n",strp->i);
-
- printf("%d\n",Get_Length(head));
- printf("\n");
- for(i=1; i<=2*Get_Length(head); i++)
- {
- strp = (str* )Get_CircleListNode(head, i);
- printf("%d\n",strp->i);
- }
-
- printf("\n");
- printf("%d\n",Get_Length(head));
- strp = (str* )Del_CircleListNode(head, 1);
- printf("%d\n",strp->i);
-
- printf("%d\n",Get_Length(head));
- printf("\n");
- for(i=1; i<=2*Get_Length(head); i++)
- {
- strp = (str* )Get_CircleListNode(head, i);
- printf("%d\n",strp->i);
- }
-
-
-
-
- printf("\n");
- for(i=1; i<=3; i++)
- {
- strp = (str* )Del_CircleListNode(head, 1);
- printf("%d\n",strp->i);
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Destroy_CircleListHead(head);
- return 0;
- }
本节知识点:
1.为何选择双向链表:由于单向链表只能一直指向下一个链表元素,不能得到前一个元素,若是要进行逆序访问操做是极其耗时的,因此引入双向链表。
2.双向链表的结构:在单向链表的基础上增长了一个链表结构pre,如图。
注意:链表第一个元素的前驱pre不是指向头结点head,而是指向NULL,链表尾结点的后继next指向NULL
3.如何将一个单向链表改为双向链表:
第一步 (改变链表的结构加入前驱):
- struct Str_DLinkList
- {
- DLinkListNode* next;
- DLinkListNode* pre;
- };
第二步 (改写插入函数):
对于一个尾插法,如图:
(1).正常的链表插入操做,代码以下:
- for(i=1; ( (i<pos) && (node->next != NULL) ); i++)
- {
- node = node->next;
- }
-
- Node -> next = node -> next;
- node -> next = Node;
(2).把刚刚插入的数据的前驱pre跟前一个数据元素相连,代码以下:
对于一个正常插入,如图:
(1).正常的链表插入操做,代码以下:
- for(i=1; ( (i<pos) && (node->next != NULL) ); i++)
- {
- node = node->next;
- }
-
- Node -> next = node -> next;
- node -> next = Node;
(2).先判断是否是尾插法,若是是尾插法,就像上一个状况同样,就不进行这一步的操做了,代码以下:
- if(NULL != Node->next)
- {
- Node->next->pre = Node;
- }
(3).把刚刚插入的数据的前驱pre跟前一个数据元素相连,代码以下:
对于一个头插法,如图:
(1).正常的链表插入操做,代码以下:
- for(i=1; ( (i<pos) && (node->next != NULL) ); i++)