此文为FishC大佬《数据结构与算法》线性表的笔记整理。
线性表定义
线性表(List):由零个或多个数据元素组成的有限序列。
这里需要强调几个关键的地方:
- 首先,它是一个序列,也就是说元素之间是有顺序的
- 若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他元素有且仅有一个前驱和后继。
- 线性表强调是有限的。
数据类型
数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。
例如整型、浮点型、字符型这些指的就是数据类型。
我们对已有的数据类型进行抽象,就有了抽象数据类型。
抽象数据类型
抽象数据类型(Abstract Data Type,ADT)是指一个数学模型及定义在该模型上的一组操作。
抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关。
描述抽象数据类型的标准格式:
ADT 抽象数据类型
DATA
数据元素之间逻辑关系的定义
Operation
操作
endADT
线性表的抽象数据类型
线性表的抽象数据类型定义:
ADT 线性表(List)
DATA
线性表的数据对象集合为{a1,a2,...,an},每个元素的类型均为DataType。
其中,除第一个元素a1外,每一个元素有且仅有一个直接前驱元素,除了最后
一个元素an外,每一个元素有且仅有一个直接后继元素。数据元素之间的关系
是一对一的关系。
Operation
InitList(*L):初始化操作,建立一个空的线性表L。
ListEmpty(L):判断线性表是否为空表,若线性表为空,返回true,否则返回false。
ClearList(*L):将线性表清空。
GetElem(L,i,*e):将线性表L中的第i个位置元素值返回给e。
LocateElem(L,e):在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号
表示成功;否则,返回0表示失败。
ListInsert(*L,i,e):在线性表L中第i个位置插入新元素e。
ListDelete(*L,i,*e):删除线性表L中第i个位置元素,并用e返回其值。
ListLength(L):返回线性表L的元素个数。
endADT
// La表示A集合,Lb表示B集合。
void unionL(List *La, list Lb)
{
int La_len, Lb_len, i;
ElemType e;
La_len = ListLength(*La);
Lb_len = ListLength(Lb);
for( i=1; i <= Lb_len; i++ )
{
GetElem(Lb, i, &e);
if( !LocateElem(*La, e) )
{
ListInsert(La, ++La_len, e);
}
}
}
线性表的顺序存储结构
线性表有两种物存储结构:顺序存储结构和链式存储结构。
线性表的顺序存储结构,指的是用过一段地址连续的存储单元依次存储线性表的数据元素。
假设线性表(a1,a2,...,an)的顺序存储如下:
a1 | a2 | a3 | a4 | ... | ai-1 | ai | ai+1 | ... | an |
是不是让人联想到了数组呢。
物理上的存储方式事实上就是在内存中找个初始地址,然后通过占位的形式,把一定的内存空间给占了,然后不啊相同数据类型的数据元素依次放在这块空地上。
线性表的顺序存储结构代码:
#define MAXSIZE 20
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];
int length; //线性表当前长度
}SqList;
可以看出,这里只是封装了一个结构,事实上就是对数组进行了封装,增加了一个当前长度的变量罢了。
总结下,顺序存储结构封装需要三个属性:
- 存储空间的起始位置,数组data,它的存储位置就是线性表存储空间的存储位置。
- 线性表的最大存储容量:数组的长度MaxSize。
- 线性表的当前长度:length
假设ElemType占用的是c个存储单元(字节),那么线性表总第i+1个数据元素和第i个数据元素的存储位置的关系是(LOC表示获得存储位置的函数):LOC(ai+1) = LOC(ai) + c
地址计算方法
所以对于第i个元素ai的存储位置可以由a1推算出来:
LOC(ai) = LOC(a1) + (i-1) * c
元素 | a1 | a2 | ... | ai-1 | ai | ... | an | 空闲空间 |
下标 | 0 | 1 | ... | i-2 | i-1 | ... | n-1 |
通过这个公式,可以随时计算出线性表中任意位置的元素,不管它是第一个还是最后一个,都是相同的时间。那么它的存储时间性能淡然就是O(1),我们通常称为随机存储结构。
获得元素操作
实现GetElem的具体操作,即将线性表L中 的第i个位置元素值返回。就程序而言,我们只需要把数组第i-1下标的值返回即可。
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
// Status 是函数的类型,其值是函数结果状态代码,如OK等。
// 初始条件:顺序线性表L已存在,1 <= i <= ListLength(L)
// 操作结果:用e返回L中第i个数据元素的值。
Status GetElem(SqList L, int i, ElemType *e)
{
if( L.length==0 || i<1 || i>L.length )
{
return ERROR;
}
*e = L.data[i-1];
return OK;
}
插入操作
插入算法的思路:
- 如果插入的位置不合理,抛出异常。
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加数组容量。
- 从最后uyige元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置。
- 将要插入的元素填入位置i处。
- 线性表长度+1
/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L)。 */
/* 操作结果:在L中第i个位置之前插入新的数据元素e,L长度+1。*/
Status ListInsert(SqList *L, int i, ElemType e)
{
int k;
if( L->length == MAXSIZE ) // 顺序线性表已经满了
{
return ERROR;
}
if( i<1 || i>L->length+1) // 当i不在范围内时
{
return ERROR;
}
if( i <= L->length ) // 若插入数据位置不在表尾
{
/* 将要插入位置后数据元素向后移动一位 */
for( k=L->length-1; k >= i-1; k-- )
{
L->data[k+1] = L->data[k];
}
}
L->data[i-1] = e; // 将新元素插入
L->length++;
return OK;
}
删除操作
- 如果删除位置不合理,抛出异常;
- 取出删除元素;
- 从删除元素位置开始便利到最后一个元素位置,分别将它们都向前移动一个位置;
- 表长-1。
/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度-1 */
Status ListDelete(SqList *L, int i, ElemType *e)
{
int k;
if( L->length == 0 )
{
return ERROR;
}
if( i<1 || i>L->length )
{
return ERROR;
}
*e = L->data[i-1];
if( i < L->length )
{
for( k=i; k < L->length; k++ )
{
L->data[k-1] = L->data[k];
}
}
L->length--;
return OK;
}
现在来分析一下,插入和删除的时间复杂度。
最好的情况:此时都时间复杂度都为O(1)
最坏的情况:O(n)
平均复杂度:简化后还是O(n)
线性表顺序存储结构的优缺点
线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1)。而在插入或删除时,时间复杂度都是O(n)
这就说明,它比较适合元素个数比较稳定,不经常插入和删除元素,而更多的操作是读取数据的应用。
优点:
- 无需为表示表中元素之间的逻辑关系而增加额外的存储空间
- 可以快速地存取表中任意位置的元素
缺点:
- 插入和删除粗啊哦做需要移动大量元素。
- 当线性表长度变化较大时,难以确定存储空间的容量。
- 容易造成存储空间的“碎片”。
线性表的链式存储结构
用一组任意的存储单元存储线性表的数据元素,这组存储单元可以存在内存中未被占用的任意位置。
相比顺序存储结构每个数据元素只需要存出一个位置就ok了,链式存储结构中除了要存储数据元素信息外,还需要存储他的后继元素的存储地址(指针)
分为数据域和指针域,这两部分信息组成数据元素称为存储映像,称为结点(Node)
n个结点链接成一个链表,每个结点只包含一个指针域,那么就称其为单链表
我们把链表的第一个结点的存储位置叫做头指针,最后一个结点指针为空(NULL)
头结点的数据域一般是不存储任何信息的。
头指针:
- 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
- 头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字)
- 无论链表是否为空,头指针均不为空
- 头指针是 链表的必要元素
头结点:
- 头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据与一般无意义(但也可以用来存放链表的长度)
- 有了头结点,对在定义元素结点前插入结点和删除第一节点起操作与其他结点的操作就统一了。
- 头结点不一定是链表的必须要素
单链表图例:
空链表图例:
我们在C语言中可以用结构指针来描述单链表。
typedef struct Node
{
ElemType data; //数据域
struct Node* Next; //指针域
}Node;
typedef struct Node* LinkList;
假设p是指向线性表第i个元素的指针,则改结点ai的数据域我们可以用p->data的值是一个数据元素,结点ai的指针域可以通过p->next来表示。p->next的值是一个指针。
p->next指向ai+1的指针。
问题:如果p->data = ai , 那么p->next -> data = ?
答案:p ->next -> data = ai+1;
单链表的读取
在单链表中,由于第i个元素到底在哪?我们压根没办法一开始就知道,所以必须得从第一个结点开始挨个地找。
算法思路:
- 声明一个结点p指向链表的第一个结点,初始化j从1开始;
- 当j < i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j+1;
- 若到链表末尾p 为空,则说明第i个元素不存在;
- 否则查找成功,返回结点p的数据。
/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:用e返回L中第i个数据元素的值 */
Status GetElem( LinkList L, int i, ElemType *e )
{
int j;
LinkList p;
p = L->next;
j = 1;
while( p && j<i )
{
p = p->next;
++j;
}
if( !p || j>i )
{
return ERROR;
}
*e = p->data;
return OK;
}
说白了就是从头开始找,知道第i个元素位置。
有最坏情况下时间复杂度为O(n),其核心思想叫做“工作指针后移”,这其实也是很多算法的常用技术。
单链表的插入
只要做一点点改变:
s->next = p->next;
p-next = s;
这两句代码的顺序不可以交换。
单链表第i个数据插入结点的算法思路:
- 声明一结点p指向链表头结点,初始化j从1开始;
- 当j<1时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j+1;
- 若到链表末为p为空,则说明第i个元素不存在;
- 否则查找成功,在系统中生成一个空节点s;
- 将数据元素e赋值给s->data;
- 单链表的插入刚刚两个标准语句;
- 返回成功
/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */
Status ListInsert(LinkList *L, int i, ElemType e)
{
int j;
LinkList p, s;
p = *L;
j = 1;
while( p && j<i ) // 用于寻找第i个结点
{
p = p->next;
j++;
}
if( !p || j>i )
{
return ERROR;
}
s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
单链表的删除
假设元素a2的结点为q,要实现结点q删除单链表的操作,其实就是讲它的前继结点的指针绕过指向后继结点即可。
p->next = p->next-next
算法思路:
- 声明结点p指向链表的第一个结点,初始化j = 1;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j+1;
- 若到链表末为p为空,则说明第i个元素不存在;
- 否则查找成功,将欲删除的结点p->next 赋值给q;
- 单链表的删除标准语句p->next = q->next;
- 将q结点中的数据赋值给e,作为返回。
- 释放q结点。
/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度-1 */
Status ListDelete(LinkList *L, int i, ElemType *e)
{
int j;
LinkList p, q;
p = *L;
j = 1;
while( p->next && j<i )
{
p = p->next;
++j;
}
if( !(p->next) || j>i )
{
return ERROR;
}
q = p->next;
p->next = q->next;
*e = q->data;
free(q);
return OK;
}
无论是单链表的插入还是删除,她们的时间复杂度都是O(n)
如果多次插入,对于顺序存储结构而言,每一次插入都是O(n)
而单链表只有第一次是O(n),接下来只是简单地通过赋值移动指针,时间复杂度为O(1)
对于插入和删除数据越频繁的操作,单链表的优势就越明显。
单链表的整表创建
对于顺序存储结构的线性表的整表创建,我们可以用数组的初始化来直观理解。
而单链表的数据是分散在内存每个角落的,他的增长也是动态的,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。
创建单链表的过程是一个动态生成链表的过程,从“空表”的初始状态起,一次建立各元素节点并逐个插入链表。
所以单链表整表创建的算法思路如下:
- 声明一结点p和计数器变量i;
- 初始化以空链表
- 让L的头结点的指针指向NULL,,即建立一个带头结点的单链表。
- 循环实现后继几点的赋值和插入。
头插法建立单链表
头插法从一个空表开始,生成新结点,读取数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,直到结束为止。简单来说,就是把新加入的元素放在表头后的第一个位置:
- 先让新节点的next指向头结点之后
- 然后把表头的next指向新节点
/* 头插法建立单链表示例 */
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0)); // 初始化随机数种子
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
for( i=0; i < n; i++ )
{
p = (LinkList)malloc(sizeof(Node)); // 生成新结点
p->data = rand()%100+1;
p->next = (*L)->next;
(*L)->next = p;
}
}
尾插法建立单链表
头插法虽然算法简单,但生成的链表中结点的次序和输入的顺序相反。
/* 尾插法建立单链表演示 */
void CreateListTail(LinkList *L, int n)
{
LinkList p, r;
int i;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node));
r = *L; //指向尾部
for( i=0; i < n; i++ )
{
p = (Node *)malloc(sizeof(Node));
p->data = rand()%100+1;
r->next = p;
r = p; // 备注:初学者可能很难理解这句,重点解释。结点既是p也是r
}
r->next = NULL;
}
单链表的整表删除
算法思路如下:
- 声明结点p和q;
- 将第一个结点赋值给p,下一个结点赋值给q;
- 循环执行释放p和将q赋值给p的操作。
Status ClearList(LinkList *L)
{
LinkList p, q;
p = (*L)->next;
while(p)
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;
return OK;
}
单链表结构与顺序存储结构优缺点
我们分别从分配方式、时间性能、空间性能三个方面进行比较。
存储分配方式
- 顺序存储结构用一段连续的存储单元一次存储线性表的数据元素
- 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
时间性能
查找:
- 顺序存储结构O(1)
- 单链表O(n)
插入和删除:
- 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
- 单链表在计算出某位置的指针后,插入和删除时间仅为O(1)
空间性能
- 顺序存储结构需要预先分配存储空间,分大了,容易造成空间浪费,分小了容易发生溢出。
- 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制
综上所述,我们得到一些经验性的结论:
- 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。
- 若需要频繁插入和删除时,宜采用单链表结构。
静态链表
用数组描述链表叫做静态链表,这种描述方法叫做游标实现法。
线性表的静态链表存储结构
#define MAXSIZE 1000
typedef struct
{
ElemType data; //数据
int cur; //游标(Cursor)
}Component, StaticLinkList[MAXSIZE];
对静态链表进行初始化相当于初始化数组:
Status InitList(StaticLinkList space)
{
int i;
for( i=0; i < MAXSIZE-1; i++)
space[i].cur = i + 1;
space[MAXSIZE-1].cur = 0;
return OK;
}
备忘录
- 对数组的第一个和最后一个元素做特殊处理,他们的data不存放数据
- 我们通常把未使用的数组元素成为备用链表
- 数组的第一个元素,即下标为0的那个元素的cur就存放备用链表的第一个结点的下标
- 数组的最后一个元素,即下标为MAXSIZE-1的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点的作用。
- 游标5指向第一个没有数据的下标
- 游标1指向第一个有数据的下标,相当于单链表的头结点作用
- 最后一个元素的游标指向0来说明下一个元素是空的
静态链表的插入操作
在静态链表中,操作的是数组,不存在像动态链表的结点申请和释放的问题,所以我们需要实现这两个函数才能够做到插入和删除操作。
为了辨明数组中哪些分量未被使用,解决的方法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表。
每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。
首先是获得空闲分量的下标:
int Malloc_SLL(StaticLinkList space)
{
int i = space[0].cur; //i = space[0].cur = 5
if( space[0].cur)
space[0].cur = space[i].cur //space[0].cur = space[i].cur = space[5].cur = 6
//把它的下一个分量作为备用。
return i;
}
/* 在静态链表L中第i个元素之前插入新的数据元素e */
Status ListInsert( StaticLinkList L, int i, ElemType e )
{
int j, k, l;
k = MAX_SIZE - 1; // 数组的最后一个元素
if( i<1 || i>ListLength(L)+1 )
{
return ERROR;
}
j = Malloc_SLL(L); //j = 5 , 此时space[0].cur = 6
if( j ) //如果j≠0
{
L[j].data = e; //把L[j]的data也就是L[5]的数据B赋值给变量e
for( l=1; l <= i-1; l++ ) //i = 2
{
k = L[k].cur; //k = L[k].cur = L[999].cur = 1
}
L[j].cur = L[k].cur; //L[j].cur = L[5].cur = L[k].cur = L[1].cur = 2
L[k].cur = j; //L[k].cur = L[1].cur = j = 5
return OK;
}
return ERROR;
}
静态链表的删除操作
如果删除C数据的话,应该如何操作呢?
/* 删除在L中的第i个数据元素 */
Status ListDelete(StaticLinkList L, int i)
{
int j, k;
if( i<1 || i>ListLength(L) )
{
return ERROR;
}
k = MAX_SIZE - 1; //k=999
for( j=1; j <= i-1; j++ ) //j = i-1 = 3-1 = 2
{
k = L[k].cur; // k1 = 1, k2 = 5
}
j = L[k].cur; // j = 2
L[k].cur = L[j].cur;
Free_SLL(L, j);
return OK;
}
/* 将下标为k的空闲结点回收到备用链表 */
void Free_SLL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur;
space[0].cur = k;
}
/* 返回L中数据元素个数 */
int ListLength(StaticLinkList L)
{
int j = 0;
int i = L[MAXSIZE-1].cur;
while(i)
{
i = L[i].cur;
j++;
}
return j;
}
静态链表的优缺点总结
优点:
- 在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点
缺点:
- 没有解决连续存储分配(数组)带来的表长难以确定的问题。
- 失去了顺序存储结构随机存取的特性。
单链表小结腾讯面试题
题目:快速找到未知长度单链表的中间结点。
普通方法:首先遍历一遍单链表以确定单链表的长度L,然后再次从头结点出发循环L/2次找到单链表的中间结点。
算法的复杂度为:O(L+L/2)= O(3L/2)
能否再优化一下呢???
有一个很巧妙的方法:利用快慢指针!
利用快慢指针原理:设置两个指针*search、*mid都指向单链表的头结点。其中*search的移动速度是*mid的两倍。当*search指向末尾结点的时候,mid正好就在中间了,这也是标尺的思想。
以下代码实现了创建一个长度为20的单链表,并通过快慢指针查询中间结点。
#include "stdio.h"
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType; /* ElemType类型根据实际情况而定,这里假设为int */
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList; /* 定义LinkList */
Status visit(ElemType c)
{
printf("%d ",c);
return OK;
}
/* 初始化顺序线性表 */
Status InitList(LinkList *L)
{
*L=(LinkList)malloc(sizeof(Node)); /* 产生头结点,并使L指向此头结点 */
if(!(*L)) /* 存储分配失败 */
{
return ERROR;
}
(*L)->next=NULL; /* 指针域为空 */
return OK;
}
/* 初始条件:顺序线性表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(LinkList L)
{
int i=0;
LinkList p=L->next; /* p指向第一个结点 */
while(p)
{
i++;
p=p->next;
}
return i;
}
/* 初始条件:顺序线性表L已存在 */
/* 操作结果:依次对L的每个数据元素输出 */
Status ListTraverse(LinkList L)
{
LinkList p=L->next;
while(p)
{
visit(p->data);
p = p->next;
}
printf("\n");
return OK;
}
/* 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */
void CreateListTail(LinkList *L, int n)
{
LinkList p,r;
int i;
srand(time(0)); /* 初始化随机数种子 */
*L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */
r=*L; /* r为指向尾部的结点 */
for (i=0; i < n; i++)
{
p = (Node *)malloc(sizeof(Node)); /* 生成新结点 */
p->data = rand()%100+1; /* 随机生成100以内的数字 */
r->next=p; /* 将表尾终端结点的指针指向新结点 */
r = p; /* 将当前的新结点定义为表尾终端结点 */
}
r->next = NULL; /* 表示当前链表结束 */
// 创建有环链表
//r->next = p;
}
Status GetMidNode(LinkList L, ElemType *e)
{
LinkList search, mid;
mid = search = L;
while (search->next != NULL)
{
//search移动的速度是 mid 的2倍
if (search->next->next != NULL)
{
search = search->next->next;
mid = mid->next;
}
else
{
search = search->next;
}
}
*e = mid->data;
return OK;
}
int main()
{
LinkList L;
Status i;
char opp;
ElemType e;
int find;
int tmp;
i=InitList(&L);
printf("初始化L后:ListLength(L)=%d\n",ListLength(L));
printf("\n1.查看链表 \n2.创建链表(尾插法) \n3.链表长度 \n4.中间结点值 \n0.退出 \n请选择你的操作:\n");
while(opp != '0')
{
scanf("%c",&opp);
switch(opp)
{
case '1':
ListTraverse(L);
printf("\n");
break;
case '2':
CreateListTail(&L,20);
printf("整体创建L的元素(尾插法):\n");
ListTraverse(L);
printf("\n");
break;
case '3':
//clearList(pHead); //清空链表
printf("ListLength(L)=%d \n",ListLength(L));
printf("\n");
break;
case '4':
//GetNthNodeFromBack(L,find,&e);
GetMidNode(L, &e);
printf("链表中间结点的值为:%d\n", e);
//ListTraverse(L);
printf("\n");
break;
case '0':
exit(0);
}
}
}
循环链表
对于单链表,由于每个结点都之存储了向后的指针,到了尾部标识就停止了向后链的操作。也就是说,按照这种方式,只能索引后继结点而不能索引前驱结点。
这会带来什么问题呢?
例如不从头结点触发,就无法访问到全部结点。
所以,将单链表中终端结点的指针端从空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
注:这里并不是说循环链表一定要有头结点。
其实循环链表和单链表的主要差异体现在循环的判断空链表的条件上,原来判断head->next是否为NULL,现在则是head->next是否等于head。
由于终端结点用尾指针rear指示,则查找终端结点是O(1)而开始结点是rear->next->next 当然也是O(1)
约瑟夫问题
//n个人围圈报数,报m出列,最后剩下的是几号?
#include <stdio.h>
#include <stdlib.h>
typedef struct node
{
int data;
struct node *next;
}node;
node *create(int n)
{
node *p = NULL, *head;
head = (node*)malloc(sizeof (node ));
p = head;
node *s;
int i = 1;
if( 0 != n )
{
while( i <= n )
{
s = (node *)malloc(sizeof (node));
s->data = i++; // 为循环链表初始化,第一个结点为1,第二个结点为2。
p->next = s;
p = s;
}
s->next = head->next;
}
free(head);
return s->next ;
}
int main()
{
int n = 41;
int m = 3;
int i;
node *p = create(n);
node *temp;
m %= n; // m在这里是等于2
while (p != p->next )
{
for (i = 1; i < m-1; i++)
{
p = p->next ;
}
printf("%d->", p->next->data );
temp = p->next ; //删除第m个节点
p->next = temp->next ;
free(temp);
p = p->next ;
}
printf("%d\n", p->data );
return 0;
}
循环链表的特点
增加一个尾指针来改进循环链表。
所以访问循环链表的最后一个元素的时间复杂度为O(1),访问第一个元素的时间复杂度也为O(1)
按照这个逻辑,判断是否为空链表的条件为:
rear 是否等于 rear-next。
两个单链表如何连接。
//假设A,B为非空循环链表的尾指针
LinkList Connect(LinkList A,LinkList B)
{
LinkList p = A->next; //保存A表的头结点位置
A->next = B->next->next; //B表的开始结点链接到A表尾
free(B->next); //释放B表的头结点,初学者容易忘记
B->next = p;
return B; //返回新循环链表的尾指针
}
判断单链表是否有环
主要有两种方法:
方法一: 使用p、q两个指针,p总是往前走,但q每次都从头开始走,对于每个结点,看p走的步数是否与q一样。如果,当p从6走到3时,用了6步,此时若q从head出发,则只需要两步到3,因而步数不等,出现矛盾,存在环。
方法二:使用p、q两个指针,p每次向前走一步,q每次向前走两步,若在某个时候p == q,则存在环。
双向链表
typedef struct DualNode
{
ElemType data;
struct DualNode *prior; //前驱结点
struct DualNode *next; //后继结点
}DualNode,*DuLinkList;
双向链表的节点结构:
双向链表的插入操作:
s->next = p;
s->prior = p->prior;
p->prior->next = s;
p->prior = s;
双向链表的删除:
p->prior->next = p->next;
p->next-prior = p->prior;
free(p)
初步笔记整理,具体视频请上B站搜索小甲鱼:《数据结构与算法》