线性表定义:由零个或多个数据元素组成的有限序列。
· 需注意几个关键点:
-它是一个序列,也就是说元素之间是有先来后到的。
-若元素存在多个,则第一个元素无前驱,而最后一个元素无后继,其他元素有且只有一个前驱和一个后继。
-线性表强调是有限的,无论计算机发展到多强大,它所处理的元素是有限的。
如果用数学语言来定义,可如下定义:
· 若将线性表记为(a1,...,ai-1,ai,ai+1,... , an)
则表中ai-1领先于ai, ai领先于ai+1, 称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。
· 线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表。
例 1 现有两个线性表LA和LB,求A和B的并集。
操作:扩大线性表LA,讲存在于LB的元素而且不存在LA的元素插入到LA中去。
//La表示A集合,Lb表示B集合 //里面的部分方法详细内容会在后面详细展示 void union(List &La, List Lb){ int La_length, Lb_length, i; ElemType e; //求线性表长度 La_length = ListLength(La); Lb_length = ListLength(Lb); for(i=1; i<=Lb_length; i++){ //取Lb中第i个数据元素赋给e GetElem(Lb,i,e); //判断该数据元素在La中是否存在 if(!LocateElem(La,e)) //不存在的话,把元素e放入La // ++La_length,表示先增加长度,再放入元素 ListInsert(La, ++La_length, e); } }
线性表的存储结构:
· 线性表有两种存储结构:顺序存储结构和链式存储结构。
· 顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。(类似数组)
物理上,该存储结构就是在内存中找个初始地址,然后通过占位的形式,把一定的内存
空间给占了,然后把相同数据类型的数据元素依次放在这块空地中。
注意:涉及到malloc和realloc要加头文件 #include<stdlib.h>
线性表顺序存储的结构代码://动态分配
#define LIST_INIT_SIZE 100 //线性表存储空间的初始分配量 #define LISTINCREMENT 10 //线性表存储空间的分配增量 typedef int ElemType; //自定义元素类型 (ElemType为int) typedef struct{ ElemType *elem; //存储空间基址 ,静态分配可改为data[LIST_INIT_SIZE] int length; //当前长度 int listsize;//当前分配的存储容量(以sizeof(ElemType)为单位) }SqList;
初始化线性表InitList_Sq:
void InitList_Sq(Sqlist &L) { //构造一个空的线性表L L.elem = (ElemType *) malloc(LIST_INIT_SIZE * sizeof(ElemType)); if (!L.elem)exit(0); //存储分配失败 L.length = 0; //空表长度为零 L.listsize = LIST_INIT_SIZE;//初始最大存储容量 }
在这种存储结构中,容易实现线性表的某些操作,如随机存取第i个数据元素等。
需注意:C语言中数组的下标从“0”开始,因此,线性表的第i个数据元素是L.elem[i-1]。
在一个线性表中,i位置插入一个数据元素,需要将i位置至最后一个数据元素往后挪一个位置,
从最后一个数据元素开始挪,倒叙遍历至第i个位置,使i位置腾出位置,然后插入元素。
所以插入算法思路:
-如果插入位置不合理,抛出异常
-如果线性表已满,动态增加数组容量
-从最后一个元素开始,倒序遍历到第i个位置,分别将它们都往后移动一个位置
-将要插入的元素填入i处
-线性表长+1
线性表插入元素://ListInsert_Sq
int ListInsert_Sq(Sqlist &L, int i, ElemType e) { //在顺序线性表L中,在第I个位置插入新的元素e //i的合法值为1<=i<=L.length+1 ElemType *newbase; ElemType *q, *p; if (i < 1 || i > L.length + 1) return 0; //元素不合法 if (L.length >= L.listsize) {//超出最大容量,需要扩容 newbase = (ElemType *) realloc(L.elem, (L.listsize + LISTINCREMENT) * sizeof(ElemType)); if (!newbase) return 0; //存储分配失败 L.elem = newbase; //新地址 L.listsize += LISTINCREMENT;//增加存储容量 } q = &(L.elem[i - 1]); for (p = &(L.elem[L.length - 1]); p >= q; --p) { *(p + 1) = *p; } *q = e; //插入e ++L.length; //表长+1 return 1; }
有了插入算法的基础,删除算法就相对简单了。
删除算法的思路:
-如果取出位置不合理,抛出异常。
-取出删除元素。
-从删除元素位置后面一个开始遍历到最后一个元素位置,分别将他们向前移一个位置。
-表长-1。
线性表删除元素://ListDelete_Sq
int ListDelete_Sq(Sqlist &L, int i, ElemType &e) { //在删除线性表L中删除第i个元素,并用e返回其值 //i的合法值为1<=i<=ListLength_Sq(L) ElemType *p, *q; if ((i < 1) || (i > L.length)) return 0; //i值不合法 p = &(L.elem[i - 1]);//p为被删除元素的位置 e = *p; //被删除元素的值赋给e q = L.elem + L.length;//表尾后面的一个地址 for (; p < q; ++p) //从删除位置的后面一个开始 *(p - 1) = *p;//被删除元素之后的所有元素左移 --L.length;//表长-1 return 1; }
有了以上两个算法为基础LocateElem_Sq和MergeList_Sq简单带过了,读者自行了解
int LocateElem_Sq(Sqlist L, ElemType e) { //找到e在L中的位置并返回,没有找到则返回0 int i = 1; //第一个元素的位序 ElemType *p = L.elem; //第一个元素的位置 for (; i < L.length + 1; ++i) { if (*p == e) return i; p++; } return 0; } void MergeList_Sq(Sqlist La, Sqlist Lb, Sqlist &Lc) { //归并两个有序表,以从小到大为例 //Lc不需要初始生成,下面代码会根据La和Lb的大小合理分配 ElemType *pa, *pb, *pc, *pa_last, *pb_last; pa = La.elem; pb = Lb.elem; Lc.listsize = Lc.length = La.length + Lb.length; pc = Lc.elem = (ElemType *) malloc(Lc.listsize * sizeof(ElemType)); if (!Lc.elem) exit(0); pa_last = La.elem + La.length - 1; pb_last = Lb.elem + Lb.length - 1; while (pa <= pa_last && pb <= pb_last) { if (*pa > *pb) { *pc = *pb; pb++; pc++; //也可以写成 *pc++ = *pb++; //意思是先把 *pb的值赋给*pc,然后两个地址都++ } else if (*pa < *pb) { *pc++ = *pa++; } else { *pc++ = *pa++; pb++; } } while (pa <= pa_last)*pc++ = *pa++;//插入La中剩余元素 while (pb <= pb_last)*pc++ = *pb++;//插入Lb中剩余元素 }
线性表顺序存储结构的优缺点:
· 线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1)。
而在插入或删除时,时间复杂度都是O(0)。
· 这就说明,它比较适合元素个数比较稳定,不经常插入和删除元素,而更多的操作是存取数据的应用。
· 优点:
-无须为表示表中元素之间的逻辑关系而增加额外的存储空间。
-可以快速地存取表中任意位置的元素。
· 缺点:
-插入和删除操作需要移动大量元素。
-当线性表长度变化较大时,难以确定存储空间的容量。
-容易造成存储空间的“碎片”。
· 链式存储结构,用一组任意的存储单元存储线性表的数据元素,
这组存储单元可以存在内存中未被占用的任意位置。
除了存储本身的信息外,还需存储一个指示其直接后继的存储位置的信息(指针)。
我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。
这两部分信息组成数据元素称为存储映像,称为结点(Node)。
n个结点链接成一个链表,即为线性表的链式存储结构。
因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
头指针与头结点的异同
头指针:
-头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头节点的指针。
-头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字)。
-无论链表是否为空,头指针不为空。
-头指针是链表的必要元素。
头节点:
-头节点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,
其数据域一般无意义(但也可以用来存放链表的长度)。
-有了头节点,在对第一元素结点前插入结点和删除第一结点起与其它结点操作统一的作用。
-头结点不一定是链表的必须要素。
单链表存储结构:
//为了适应课本,这里重命名 typedef struct LNode *LinkList; typedef struct LNode{ ElemType data; //数据域 struct LNode *next; //指针域 }LNode;
· 假设p是指向线性表第i个元素的指针,则该结点ai的数据域可以用p->data来表示,
p->data的值是一个数据元素,结点ai的指针域可以用p->next表示 ,他的值是指针。
· p->next指向第i+1个元素,也就是指向ai+1的指针。
· 例子
-如果p->data 等于 ai , 那么p->next->data 等于 ai+1
单链表的读取
· 在线性表顺序存储结构中,我们要计算任意一个元素的存储位置是很容易的。
· 但是在单链表中,对于第i个元素的位置,我们一开始是无法知道的,必须从头结点诶个查找。
· 因此,对于单链表实现获取第i个元素的数据的操作GetElem,操作略麻烦。
· 获得链表第i个数据的算法思路:
-声明一个结点p指向链表第一个结点,初始化j从i开始。
-当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j++。
-若到链表末尾p为空,则说明第i个元素不存在。
-否则查找成功,返回结点p的数据。
单链表查找元素://GetElem_L
int GetElem_L(LinkList L, int i, ElemType &e) { //L为带头 结点的单链表的头指针 //当第i个元素存在时,其值赋给e并返回1,否则返回0 LinkList p; p = L->next; int j = 1;//初始化,p指向第一个结点,j为计数器 while (p && j < i) { //顺指针向后查找,直到p指向第i个元素或p为空 p = p->next; ++j; } if (!p || j > i) return 0; //第i个元素不存在 e = p->data; //返回第i个元素值 return 1; }
· 说白了,就是从头开始找,直到第i个元素为止,或者遍历完整个链表都没到第i个位置。
· 该算法时间复杂度取决于i的位置,当i=1时,则不需要遍历,而i=n则遍历n-1次才可以。
因此最坏情况的时间复杂度为O(n)。
· 由于单链表结构中没有定义表长,所以不能知道要循环多少次,也就不能用for来控制。
· 其核心思想叫做“工作指针后移”,这其实也是很多算法的常用技术。
单链表的插入
· 思考后可以发现,只需让s->next和p->next的指针做一点改变。
s->next = p->next
p->next = s
· 单链表第i个数据插入点的算法思路:
-声明一结点p指向链表头结点,初始化j从1开始。
-当就j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j++。
-若到链表末尾p为空,则说明第i个元素不存在。
-若查找成功,在系统中生成一个空结点s。
-将数据元素e赋值给s->data。
-然后插入刚才的两个语句。
-返回OK。
单链表插入算法:ListInsert_L
int ListInsert_L(LinkList &L, int i, ElemType e) { //在带头结点的单链线性表L中第i个位置之前插入元素e LinkList p, s; p = L; int j = 0;//i=1,前面的一个结点为头结点所以要从0开始 while (p && j < i - 1) {//寻找i-1个结点 p = p->next; ++j; } if (!p || j > i - 1) return 0; //i小于1或者大于表长加1 s = (LinkList) malloc(sizeof(LNode));//生成新结点 s->data = e; s->next = p->next; //插入L中 p->next = s; return 1; }
单链表删除操作
· 假设元素a2的结点为q,要实现结点q删除单链表的操作,
其实就是将它的前继结点的指针绕过后继指针即可。
· 然后我们要做的是:
-可以这样:p->next = p->next->next;
-也可以这样:q = p->next; p->next = q->next;
· 单链表第i个数据删除结点的算法思路:
-声明结点p指向链表第一个结点,初始化j=1。
-当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j++。
-若到链表末尾p为空,则说明第i个元素不存在。
-否则查找成功,将欲删除结点p->next赋值给p。
-单链表的删除标准语句p->next = q->next。
-将q结点中的数据赋值给e,作为返回。
-释放q结点。
单链表删除算法:ListDelete_L
int ListDelete_L(LinkList &L, int i, ElemType &e) { //在带头结点的单链线性表L中,删除第i个元素,并由e返回其值 LinkList p = L; LinkList q; int j = 0; while (p->next && j < i - 1) { //寻找第i个结点,并令p指向其前驱 p = p->next; ++j; } if (!(p->next) || j > i - 1) return 0; //删除位置不合理 //删除并释放结点 q = p->next; p->next = q->next; e = q->data; free(q); return 1; }
单链表存储结构效率分析:
· 我们可以发现无论是单链表插入还是删除算法,它们都是由两部分组成:
第一部分就是遍历查找第i个元素,第二部分就是实现插入和删除元素。
· 从整个算法来说,它们的时间复杂度都是O(n)。
· 由于我们不知道第i个元素的指针位置,单链表删除和插入操作和线性表的顺序存储结构是没有优势的。
· 但如果,我们需要从第i个位置开始,插入连续10个元素,对于顺序存储结构
意味着每次插入都要移动n-1个位置,所以每次都是O(n)。
· 而单链表,我们只需要在第一次时,找到第i个位置的指针,此时为O(n),
接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)。
· 显然,对于插入和删除数据越频繁的操作,单链表的效率优势就体现出来了。
单链表的整表创建:
· 对于顺序存储结构的线性表的整表创建,我们可以用数组的初始化来直观理解。
· 而单链表和顺序存储结构就不一样了,它不像顺序存储结构数据那么集中,
它的数据可以是分散在内存各个角落的,它的增长也是动态的。
· 对于每个链表来说,它所占空间的大小和位置是不需要预先分配划定的,
可以根据系统的情况和实际的需求即使生成。
· 创建单链表的过程是一个动态生成链表的过程,从“空表”的初始状态起,
依次建立各元素结点并逐个插入链表。
· 所以单链表整表创建思路如下:
-声明一结点p和计数器变量i。
-初始化一空链表L。
-让L的头结点指针指向NULL,即建立一个带头结点的单链表。
-循环实现后继结点的赋值和插入。
1. 头插法建立单链表
· 头插法从一个空表开始,生成新结点,读取数据放到新结点的数据域中,
然后讲新结点插入到当前链表的表头上,直到结束为止。
· 简单地来说,就是把新加进来的元素放在表头后的第一个位置:
-先让新结点的next指向头结点之后。
-然后让表头的next指向新结点。
· 用现实环境模拟的话就是插队的方法,始终让新结点插在第一个位置。
头插法建立到单链表算法://CreateList_L
void CreateList_L(LinkList &L, int n) { //逆序输入n个元素的值,建立带表头结点的单链线性表L int i; LinkList p; L = (LinkList) malloc(sizeof(LNode)); L->next = NULL; //先建立一个带表头结点的单链表 for (i = 0; i < n; i++) { p = (LinkList) malloc(sizeof(LNode));//生成新结点 scanf("%d", &p->data); //输入元素值 p->next = L->next; //插入到表头 L->next = p; } }
2.尾插法建立单链表
· 头插法建立单链表虽然算法简单,但生成的链表中的结点次序和输入的顺序相反(所以上例代码中要求逆序输入)。
· 把新结点插入至最后,这种算法叫尾插法。
尾插法建立到单链表算法://CreateListTail_L
void CreateListTail_L(LinkList &L, int n) { LinkList p, r; int i; L = (LinkList) malloc(sizeof(LNode)); r = L; for (i = 0; i < n; i++) { p = (LinkList) malloc(sizeof(LNode)); scanf("%d", &p->data); r->next = p; r = p; } r->next = NULL; }
单链表的整表删除:
· 当我们不打算使用该链表时,需要把它销毁,在内存中腾出空间。
· 单链表整表删除的算法思路如下:
-声明结点p和q。
-将第一个结点赋值给p,下一个结点赋值给q。
-循环执行释放p和将q赋值赋值给p的操作。
单链表整表删除算法://DestoryList
void DestoryList(LinkList &L) { LinkList p, q; p = L->next; //第一个数据结点 while (p) { q = p->next;//下一个数据结点 free(p); //释放当前结点 p = q; } L->next = NULL;//头指针指向NULL }
两个有序链表并为一个有序链表:
· 跟顺序存储链表类似,只不过改变了链表结点之间的关联性。
算法实现://MergeList_L
void MergeList_L(LinkList &La, LinkList &Lb, LinkList &Lc) { //已知单链线性表La和Lb的元素按递增排列 //求并集,得到新的单链线性表Lc,它也按递增排列 LinkList pa, pb, pc; pa = La->next; pb = Lb->next; Lc = pc = La; //用La的头结点作为Lc的头结点 while (pa && pb) { if (pa->data < pb->data) { pc->next = pa; pc = pa; pa = pa->next; } else if (pa->data > pb->data) { pc->next = pb; pc = pb; pb = pb->next; } else { pc->next = pa; pc = pa; pa = pa->next; pb = pb->next; } } pc->next = pa ? pa : pb; //插入剩余片段 free(Lb); //释放头结点 }
单链表结构与顺序存储结构优缺点:
· 存储分配方式:
-顺序存储结构用一段连续的存储单元依次存储线性表的数据元素。
-单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素。
· 时间性能:
-查找:
· 顺序存储结构O(1)。
· 单链表O(n)。
-插入和删除
· 顺序存储结构需要平均移动表长一半的元素,时间为O(n)。
· 单链表在计算出某位置的指针后,插入和删除时间仅为O(1)。
· 空间性能:
-顺序存储结构需要预分配存储空间,分大了,容易空间浪费,分小了,容易溢出。
-单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制。
· 结论:
-若线性表需要频繁查找,很少进行插入和删除操作,宜采用顺序存储结构。
-若需要频繁插入和删除操作,宜采用链表存储结构。
-当线性表中元素个数变化较大或者根本不知道有多大,最好用单链表结构,不用考虑大小。
静态链表
· 用数组描述的链表叫做 静态链表,这种描述方法叫做游标实现法。
线性表的静态单链表存储结构 :
#define MAXSIZE 1000 typedef int ElemType; typedef struct { ElemType data;//数据 int cur; //游标(cursor) }component, SLinkList[MAXSIZE];
初始化静态链表:
void InitSpace_SL(SLinkList &space){ //将一维数组space中各分量链成一个备用链表, // space[0].cur为头指针,“0”表示空指针 for (int i = 0; i < MAXSIZE - 1; ++i) { space[i].cur=i+1; } space[MAXSIZE-1].cur=0; }
注意:
- 对数组的第一个和最后一个元素作特殊处理,它们的data不存放数据。
- 通常把未使用的数组元素称为备用链表。
- 数组的第一个元素,即下标为0的那个元素cur就存放备用链表的第一个结点的下标。
- 数组的最后一个元素,即下标为MAXSIZE-1的cur,则存放第一个有数值的元素
下标,相当于单链表中的头结点作用,cur为0表示空指针。
静态链表的定位函数://Locate_SL
int LocateElem_SL(SLinkList S, ElemType e) { //在静态单链线性表L中查找第1个值为e的元素。 //若找到,则返回它在L中的位序,否则返回0 int i = S[0].cur; //i指示表中第一个结点 while (i && S[i].data != e)//在链表中查找 i = S[i].cur; return i; }
静态链表的插入操作:
· 在静态链表中,操作的是数组,不存在像动态链表的结点申请和释放的问题,
所以我们需要自己实现这两个函数,才可以做到插入和删除操作。
· 为了辨明数组中哪些分量未被使用,解决的方法是讲所有未被使用过的及
已被删除的用游标链成一个备用链表。
· 每当插入时,便可以从备用链表上去的第一个结点作为待插入的新结点。
我们这里假设要把B插入到A后面:
首先要获得空闲分量的下标://Malloc_SL
int Malloc_SL(SLinkList &space){ //若备用空间链表非空,则返回分配的结点下标,否则 返回0 //i为返回待插入数据的结点,把space[0].cur赋值为待插入 // 结点的cur,即第二个空闲结点 int i=space[0].cur; if(space[0].cur) space[0].cur=space[i].cur;//腾出空闲的第一个位置 return i; }
计算链表的实际长度(含有data数据的结点)://ListLength
int ListLength(SLinkList &L){ //计算存放data的个数 //返回个数 int j=0; int i=L[MAXSIZE-1].cur;//得到头位置,相当于头指针 while (i){ i=L[i].cur; j++; } return j; }
静态链表的插入算法://ListInsert_SL
int ListInsert_SL(SLinkList &L,int i,ElemType e){ //在第i个元素前插入e int j,k; k=MAXSIZE-1;//数组的最后一个元素 if(i<1||i>ListLength(L)+1) return 0;//表示i不合法 j=Malloc_SL(L); if(j){ L[j].data=e; //把e放入待插入结点 for (int l = 1; l < i; ++l) { k=L[k].cur;//找到i前面一个的元素的游标 } L[j].cur=L[k].cur;//将它的游标赋值给插入点的游标 L[k].cur=j; //把插入点的下标赋值给前面那个元素的游标 return 1; } return 0; }
静态链表的删除操作:
首先,需要把删除的结点手动回收://Free_SL
void Free_SL(SLinkList &space, int k) { //将下标为k的空闲结点回收到备用链表 space[k].cur = space[0].cur; space[0].cur = k; }
然后,静态链表的删除算法://ListDelete_SL
int ListDelete_SL(SLinkList &L, int i) { int j, k; if (i < 1 || i > ListLength(L)) return 0;//表示i不合法 k = MAXSIZE - 1; for (j = 1; j < i; ++j) { k = L[k].cur;//找到需要删除数据的结点的前一个 } j = L[k].cur;//需要删除的结点位置 L[k].cur = L[j].cur;//使删除结点的前后连起来 Free_SL(L, j);//回收至备用链表 return 1; }
书本中求集合(A-B)并(B-A)算法://difference
void difference(SLinkList &space, int &S) { //依次输入集合A和B的元素,在一维数组space中建立表示结合(A-B)并(B-A) //的静态链表,S为其头指针(i=1)。假设备用空间足够大,space[0].cur为其头指针 //从i=2开始存data InitSpace_SL(space); //初始化备用空间 S = Malloc_SL(space); //生成S头结点 int r = S; //r指向S的当前最后结点 int m, n, i, b, p, k; scanf("%d%d", &m, &n); //输入A和B的元素个数 for (int j = 0; j < m; ++j) {//建立集合A的链表 i = Malloc_SL(space); //分配结点 scanf("%d", &space[i].data);//输入A的元素值 space[r].cur=i; //插入到表尾 r = i; } space[r].cur = 0; //尾结点的指针为空 for (int j = 0; j < n; ++j) { //一次输入B的元素,若不在当前表中,则插入,否则删除 scanf("%d", &b); p = S; k = space[S].cur;//k指向A中的第一个结点 //在当前表中查找 while (k != space[r].cur && space[k].data != b) { p = k; k = space[k].cur; } //当前表中不存在该元素,插入在r所指结点之后,且r的位置不变 if (k == space[r].cur) { i = Malloc_SL(space); space[i].data = b; space[i].cur = space[r].cur; space[r].cur = i; } else {//元素已存在,删之 space[p].cur = space[k].cur; Free_SL(space, k); if (r == k)//如果删除的是r所指结点,则需修改尾指针 r = p; } } }
静态链表优缺点总结:
· 优点:
-在插入和删除时,只需要修改游标,不需要移动元素,从而改进了在顺序
存储结构中的插入和删除操作需要移动大量元素的缺点。
· 缺点:
-没有解决连续存储分配(数组)带来的表长难以确定的问题。
-失去了顺序存储结构随机存储的特性。
· 总的来说,静态链表其实是为了给没有指针的编程语言实现单链表功能的方法。
单链表课后小结,思考题 :
· 题目:快速找到未知长度单链表的中间结点。
· 我们很容易就能想到,因为单链表没有下标,只能从头遍历,先遍历一遍求出
链表长度,然后再从头结点出发循环L/2找到中间点。
· 算法复杂度为O(L+L/2)=O(3L/2)。
· 但是有没有更快的方法呢?
· 利用快慢指针原理:设置两个指针*search、*mid都是指向单链表的头结点。
其中*search的移动速度是*mid的2倍。当*search指向末尾结点的时候,
mid也正好在中间。这也是标尺的思想。
快慢指针找到单链表中间结点算法://GetMidNode
ElemType GetMidNode(LinkList L){ //寻找中间结点,返回结点中的data LinkList search,mid; mid=search=L; ElemType e; while (search->next!=NULL){ //search的移动速度是mid的两倍 if(search->next->next!=NULL){ search=search->next->next; mid=mid->next; } else { search = search->next; } } e = mid->data; return e; }
循环链表
· 本质就是把单链表最后的指针由空指针改为指向头结点。
· 注:这里并不是说循环链表一定要有头结点。
· 在判断是否为空表上,循环链表只要判断head->next是否等于head
· 将两表合并时,只需将一个表的表尾和另一个表的表头相接,这个操作秩序改变两个指针值,时间为O(1)。
循环链表结构定义(可根据实际需求更改,这里跟单链表结构体一样):
typedef int ElemType; typedef struct CNode *CLinkList; typedef struct CNode { ElemType data; //数据域 struct CNode *next; //指针域 } CNode;
初始化循环链表://Init_CL
// 初始化循环链表,初始化前,pNode指针需为空 void Init_CL(CLinkList &pNode) { int item; CLinkList temp, target; printf("输入结点的值,输入0完成初始化\n"); while (1) { scanf("%d", &item); if (item == 0) return;//输入0直接跳出该方法 if (pNode == NULL) { //循环链表中只有一个结点 pNode = (CLinkList) malloc(sizeof(CNode)); if (!pNode) exit(0); pNode->data = item; pNode->next = pNode; } else { //找到next指向第一个结点的结点 for (target = pNode; target->next != pNode; target = target->next); //生成一个新的结点 temp = (CLinkList) malloc(sizeof(CNode)); if (!temp) exit(0); temp->data = item; temp->next = pNode; target->next = temp; } } }
插入结点算法(需注意插入在第一个结点位置处)//Insert_CL
/* * 插入结点 * 参数,链表的第一个结点,插入的位置 * 头结点有data的循环单链表 */ void Insert_CL(CLinkList &cNode, int i) { CLinkList temp, target, p; int item; int j = 1; printf("输入要插入结点的值:"); scanf("%d", &item); if (i == 1) { //新插入的结点作为第一个结点 temp = (CLinkList) malloc(sizeof(CNode)); if (!temp) exit(0); temp->data = item; //寻找到最后一个结点 for (target = cNode; target->next != cNode; target = target->next); temp->next = cNode; target->next = temp; cNode = temp; } else { target = cNode; for (; j < (i - 1); ++j) //找到i-1的位置 target = target->next; temp = (CLinkList) malloc(sizeof(CNode)); if (!temp) exit(0); temp->data = item; //插入结点 p = target->next; target->next = temp; temp->next = p; } }
删除第i个结点://Delete_CL
//删除结点,删除第i个结点 void Delete_CL(CLinkList &pNode, int i) { CLinkList target, temp; int j = 1; if (i == 1) { //删除的是第一个结点 //找到最后一个结点 for (target = pNode; target->next != pNode; target = target->next); temp = pNode; pNode = pNode->next; target->next = pNode; free(temp); } else { target = pNode; for (; j < i - 1; ++j) target = target->next; temp = target->next; target->next = temp->next; free(temp); } }
返回结点所在位置://Search_CL
int Search_CL(CLinkList &pNode, ElemType e) { CLinkList target; int i = 1; //如果 data值不匹配而且没有遍历完,一直找下去 for (target = pNode; target->data != e && target->next != pNode; ++i) { target = target->next; } if (target->next == pNode)//表示未找到 return 0; else return i; }
约瑟夫问题
· 据说著名犹太历史学家Josephus有过一下的故事:在罗马人占领乔塔帕特之后,
39个犹太人和Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不
要被敌人抓到,于是决定了一个自杀方式,41个人围成一个圈,由第一个人开始
报数,每报数到第3人该人就自杀,然后由下面的人重新报数,直到所有人自杀。
· 然而Josephus和他的朋友并不想遵从,Josephus要朋友先假装遵从,他将朋友
与自己安排在第16个和第31个位置,于是逃过了这场死亡游戏。
· 问题:用循环链表模拟约瑟夫问题,把41人自杀的顺序按编号输出。
代码实现:
#include <cstdio> #include <stdlib.h> typedef int ElemType; typedef struct CNode *CLinkList; typedef struct CNode { ElemType data; //数据域 struct CNode *next; //指针域 } CNode; CLinkList InitYueSeFu(int n){ CLinkList p=NULL; CLinkList head,s; //生成的时候带头的,最后结束时会释放head head=(CLinkList)malloc(sizeof(CNode)); p=head; int i=1; if(n!=0){ while (i<=n){ s=(CLinkList)malloc(sizeof(CNode)); //为循环链表初始化,第一个结点为1,第二个结点为2 s->data=i++; p->next=s; p=s; } //尾部和头部第一个数据结点连起来 s->next=head->next; } free(head); //释放头结点,返回第一个数据头结点 return s->next; } int main (){ //这里n为人数,m为数的数,n必须大于m int n=41; int m=3; int i; CLinkList p=InitYueSeFu(n); CLinkList temp; while (p!=p->next){ //m-1=2这里循环一次,比如指针从1号位指向2号位,是自杀的前面一个人 for(i=1;i<m-1;i++) p=p->next; printf("%d->",p->next->data);//自杀 //temp为自杀的人 temp=p->next; p->next=temp->next; free(temp); //这里指针向后移 p=p->next; } //最后只剩最后一个数据元素结点 printf("%d\n",p->data); return 0; }
循环链表特点:
· 在单链表中,有了头结点时,可以用O(1)的时间访问第一个结点,
但对于要访问最后一个结点,我们必须要诶个向下索引,所以需要O(n)。
· 如果用到带尾指针的循环链表,用O(1)就可以访问到最后一个结点,
这样查找开始结点和终端结点都很方便,如图:
· 按照这样,就可以通过rear是否等于rear->next来判断是否空表
· 可见循环链表的特点是无需增加存储量,仅对链接方式稍作改变,即使得表处理更灵活。
题目:实现将两个线性表(a1,...,an)和(b1,...,bm)链成(a1,...,an,b1,...,bm)。
分析 :
-若在单链表或头指针表示的单循环表上做这种操作 ,都需要遍历第一个链表,
找到结点an,然后将结点b1链到an的后面,时间是O(n)。
-若用尾指针,只需修改指针,无需遍历,其时间是O(1)。
代码实现://Connect_CL
//参数为两个链表的尾指针 CLinkList Connect_CL(CLinkList rearA,CLinkList rearB){ CLinkList headA=rearA->next; //an的下个结点改为b1 rearA->next=rearB->next->next; //释放B的头结点 free(rearB->next); //B尾A头连起来 rearB->next=headA; //返回新循环链表的尾指针 return rearB; }
判断单链表中是否有环
有环的定义是,链表的尾结点指向了链表中的某个结点。
方法一:使用p、q两个指针,p总是向前走,但q每次都从头开始走,对于每个结点,
看p走的步数是否和q一样。如图,当p从6走到3时,用了6步,此时若q从head出发,
则只需两步就到3,因而步数不等,出现矛盾,存在环。
方法二:使用p、q两个指针,p每次向前走一步,q每次向前走2步,若在某个时候
p==q,则存在环。
这里不作代码展开
魔术师发牌问题
· 问题描述:魔术师利用一幅牌中的13张黑牌,预先将他们排好后叠放在一起,
牌面朝下。对观众说:“我不看牌,只数数就可以猜到每张牌是什么,我大声
数数,你们听,不信?现场演示。”魔术师将最上面的那张牌数为1,把他翻过
来正好是黑桃A,将黑桃A放在桌子上,第二次数1,2,将第一张牌放在这些
牌的下面,将第二张牌翻过来,正好是黑桃2,也将它放在桌子上这样一次进行
将13张牌全部翻出,准确无误。
· 问题:牌的开始顺序是如何安排的?
代码实现:
#include <stdio.h> #include <stdlib.h> #define CardNumber 13 typedef struct node{ int data; struct node *next; }sqlist,*Linklist; //初始化链表 Linklist CreateLinkList(){ Linklist head=NULL; Linklist s,r; r=head; //插入13个结点,没个结点data为0 for (int i = 1; i <=CardNumber ; ++i) { s=(Linklist)malloc(sizeof(sqlist)); s->data=0; if(head==NULL) head=s; else r->next=s; r=s; } r->next=head; //返回头结点 return head; } //发牌顺序计算 void Magician(Linklist &head){ Linklist p; int CountNumber=2; p=head; p->data=1;//第一张牌放1 //从2开始放 while (1){ //找到要放的位置 for (int j = 0; j < CountNumber; ++j) { p=p->next; if(p->data!=0){ //如果数据不为0,忽略这次loop,j-- p->next; j--; } } //放入数字,CountNumber++ if(p->data==0){ p->data=CountNumber; CountNumber++; //CountNumber到14时,跳出循环 if(CountNumber==14) break; } } } //销毁工作 void DestoryList(Linklist &list){ Linklist ptr=list; Linklist buff[CardNumber]; int i=0; while (i<CardNumber){ buff[i++]=ptr; ptr=ptr->next; } for ( i = 0; i < CardNumber; ++i) { free(buff[i]); } list=NULL; } int main (){ Linklist p; p=CreateLinkList(); Magician(p); printf("按如下顺序排列:\n"); for (int j = 0; j < CardNumber; ++j) { printf("黑桃%d",p->data); p=p->next; } DestoryList(p); return 0; }
双向链表
双向链表结点结构:
typedef int ElemType; typedef struct DuLNode{ ElemType data; struct DuLNode *prior; //前驱结点 struct DuLNode *next; //后继结点 }DuLNode,*DuLinkList;
当然也可以有它的双向循环链表
那么,对于链表中的某一个结点p,他的后继结点的前驱结点就是他自己
双向链表的插入操作:
· 操作并不复杂,注意其顺序,不要写反了。
· 算法思路:
- s->next = p;
- s->prior = p->prior;
- p->next->prior = s;
- p->prior = s;
· 关键在于顺序不要乱!!!
双向链表的删除操作:
· 看过上面的插入操作,删除操作相对就简单了。
· 代码思路:
- p->prior->next = p->next;
- p->next->prior = p->prior;
- free(p);
综合插入和删除的代码:
DuLinkList GetElemP_DuL(DuLinkList L,int i){ DuLinkList p=NULL; p=L; for (int j = 0; j < i; ++j) { p=p->next; } return p; } int ListInsert_DuL(DuLinkList &L,int i,ElemType e){ //在带头结点的双链循环线性表L中第i个位置之前插入元素e //i合法值为1<=i<=表长+1。 DuLinkList p,s; if(!(p=GetElemP_DuL(L,i)))//在L中确定插入位置 return 0; //p为NULL表示位置不合法 if(!(s=(DuLinkList)malloc(sizeof(DuLNode)))) return 0; s->data=e; s->prior=p->prior; p->prior->next=s; s->next=p; p->prior=s; return 1; } int ListDelete_DuL(DuLinkList &L,int i,ElemType &e){ //删除带头结点的双链循环线性表L的第i个元素,i的合法值为1<=i<=表长 DuLinkList p; if(!(p=GetElemP_DuL(L,i))) return 0; e=p->data; p->prior->next=p->next; p->next->prior=p->prior; free(p); return 1; }
· 最后小结,双向链表相对于单链表来说,是更加复杂一些,每个结点多一个prior指针,
对于插入和删除操作的顺序不要搞错。
· 双向链表可以有效提高算法的时间性能,说白了就是用空间换取时间。
这算是本人考研 复习过程中的笔记吧,欢迎批评指正。