1.复杂度分析
1.1 什么是时间复杂度和空间复杂度?
在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O
渐进表示法。1.2 如何计算常见算法的时间复杂度和空间复杂度?
使用大
O
的渐进表示法常见算法有三种情况:【一般关注的是算法的最坏运行情况】
- 最好情况:任意输入规模的最小运行次数;(下界)
- 最坏情况:任意输入规模的最大运行次数;(上界)
- 平均情况:任意输入规模的期望运行次数
2.线性表
2.1 线性表的定义
线性表是具有
相同
数据类型的n
( n ≥ 0 n \geq 0 n≥0)个数据元素的有限序列
,其中n
为表长,当 n = 0 n=0 n=0时线性表为一个空表。
除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继;
线性表的特点:相同数据类型,有限个数据元素,具有顺序性2.2 线性表的顺序表示(顺序表)
顺序表的特点:
随机访问、存储密度高(每个结点只存储数据元素)、插入删除操作较为麻烦(因逻辑上相邻的元素物理上也相邻)、顺序存储分配需要一段连续的存储空间,不够灵活
注意:线性表中元素的位序是从1开始的,而数组中元素下标是从0开始的。
//必备头文件
#include <stdio.h>
#include <stdlib.h>
/*[1]定义静态顺序表的最大长度和动态顺序表的初始默认最大容量*/
#define MaxSize 10 //静态顺序表的最大长度
#define InitSize 10 //动态顺序表的初始默认最大容量
/*[2]自定义C语言的bool变量*/
//也可忽略,直接使用int变量
#define bool char
#define true 1
#define false 0
/*[3]自定义数据元素的数据类型*/
typedef int ElemType;
/*[4]顺序表的结构体定义*/
//<1>顺序表的数据元素结构体(静态分配)
typedef struct SqList
{
//[1]用静态数组存放数据元素
ElemType data[MaxSize];
//[2]顺序表的当前长度
int length;
}SqList;
//<2>顺序表的数据元素结构体(动态分配)
typedef struct SeqList
{
//[1]使用动态指针分配数组
ElemType* data;
//[2]顺序表的当前长度
int length;
//[3]顺序表的最大容量
int maxsize;
}SeqList;
初始化、元素插入、元素删除、元素查找、空间扩展、顺序表的输出
/*[5]顺序表的初始化*/
//<1>顺序表的初始化(静态分配)
bool InitSqList(SqList* L);
//<2>顺序表的初始化(动态分配)
bool InitSeqList(SeqList* L);
/*[6]顺序表的元素插入(按位插入)*/
//<1>顺序表的元素插入(静态分配)
//在顺序表L的第i个位置插入一个新元素e;
bool SqListInsert(SqList* L, int i, ElemType e);
//<2>顺序表的元素插入(动态分配)
bool SeqListInsert(SeqList* L, int i, ElemType e);
/*[7]顺序表的元素删除(按位)*/
//<1>静态顺序表的元素删除
// 将顺序表L的第i位元素删除,并把删除的元素的值返回给e
bool SqListElemDelete(SqList* L, int i, ElemType* e);
//<2>动态顺序表的元素删除
bool SeqListElemDelete(SeqList* L, int i, ElemType* e);
/*[8]顺序表的按值查找元素*/
//<1>静态顺序表的元素按值查找,并返回其位序
int SqListLocElem(SqList L, ElemType e);
//<2>动态顺序表的元素按值查找,并返回其位序
int SeqListLocElem(SeqList L, ElemType e);
/*[9]动态顺序表的扩展*/
bool IncreaseSize(SeqList* L, int len);
/*-------------------------------------------*/
/*额外操作——顺序表的输出*/
//<1>静态顺序表的输出
bool SqListPrintf(SqList L);
//<2>动态顺序表的输出
bool SeqListPrintf(SeqList L);
/*[5]顺序表的初始化*/
//<1>顺序表的初始化(静态分配)
bool InitSqList(SqList* L)
{
//[1]初始化表长
L->length = 0;
//[2]初始化数据元素?
/*for (int i = 0; i < MaxSize; i++)
{
L->data[i] = 0;
}*/
return true;
}
//<2>顺序表的初始化(动态分配)
bool InitSeqList(SeqList* L)
{
//[1]初始化表长
L->length = 0;
//[2]初始化动态顺序表的最大容量
L->maxsize = InitSize;
//[3]初始化数据元素?
L->data = (ElemType*)malloc(sizeof(ElemType) * InitSize);
return true;
}
/*[6]顺序表的元素插入(按位插入)*/
//<1>顺序表的元素插入(静态分配)
//在顺序表L的第i个位置插入一个新元素e;
bool SqListInsert(SqList* L, int i, ElemType e)
{
//[1]判断插入操作是否合法
if (i<1 || i>L->length + 1)
{
printf("The position of the element to be inserted is invalid\n");
return false;
}
if (L->length >= MaxSize)
{
printf("This sequence list is full!\n");
return false;
}
//[2]将第i位以及第i位后的所有元素后移一位
for (int j = L->length; j >= i; j--)
{
L->data[j] = L->data[j - 1];
}
//[3]将新元素插入到正确的位置——区分数组下标和元素位序
L->data[i - 1] = e;
//[4]表长加一
L->length++;
//[5]返回true,插入成功
return true;
}
//<2>顺序表的元素插入(动态分配)
bool SeqListInsert(SeqList* L, int i, ElemType e)
{
//[1]判断插入操作是否合法
if (i<1 || i>L->length + 1)
{
printf("The position of the element to be inserted is invalid\n");
return false;
}
if (L->length >= L->maxsize)
{
printf("This sequence list is full!\n");
return false;
}
//[2]将第i位以及第i位后的所有元素后移一位
for (int j = L->length; j >= i; j--)
{
L->data[j] = L->data[j - 1];
}
//[3]将新元素插入到正确的位置——区分数组下标和元素位序
L->data[i - 1] = e;
//[4]表长加一
L->length++;
//[5]返回true,插入成功
return true;
}
/*[7]顺序表的元素删除(按位)*/
//<1>静态顺序表的元素删除
// 将顺序表L的第i位元素删除,并把删除的元素的值返回给e
bool SqListElemDelete(SqList* L, int i, ElemType* e)
{
//[1]判断操作的合法性
if (i<1 || i>L->length + 1)
{
printf("The position of the element to be inserted is invalid\n");
return false;
}
if (L->length <= 0)
{
printf("This sequence list is empty!\n");
return false;
}
//[2]将待删除的元素赋值给e
*e = L->data[i - 1];
//[3]将第i位元素以及其后面的元素往前移一位
for (int j = i; j < L->length; j++)
{
L->data[j - 1] = L->data[j];
}
//[4]表长减一
L->length--;
//返回true
return true;
}
//<2>动态顺序表的元素删除
bool SeqListElemDelete(SeqList* L, int i, ElemType* e)
{
//[1]判断操作的合法性
if (i<1 || i>L->length + 1)
{
printf("The position of the element to be inserted is invalid\n");
return false;
}
if (L->length <= 0)
{
printf("This sequence list is empty!\n");
return false;
}
//[2]将待删除的元素赋值给e
*e = L->data[i - 1];
//[3]将第i位元素以及其后面的元素往前移一位
for (int j = i; j < L->length; j++)
{
L->data[j - 1] = L->data[j];
}
//[4]表长减一
L->length--;
//返回true
return true;
}
/*[8]顺序表的按值查找元素*/
//<1>静态顺序表的元素按值查找,并返回其位序
int SqListLocElem(SqList L, ElemType e)
{
for (int i = 0; i < L.length; i++)
{
if (L.data[i] == e)
{
return i + 1;
}
}
return 0;
}
//<2>动态顺序表的元素按值查找,并返回其位序
int SeqListLocElem(SeqList L, ElemType e)
{
for (int i = 0; i < L.length; i++)
{
if (L.data[i] == e)
{
return i + 1;
}
}
return 0;
}
/*[9]动态顺序表的扩展*/
bool IncreaseSize(SeqList* L, int len)
{
//[1]生成指向原来顺序表的存储空间的指针
ElemType* p = L->data;
//[2]为顺序表开辟一块更大的空间
L->data = (ElemType*)malloc(sizeof(ElemType) * (L->maxsize + len));
//[3]转移数据
for (int i = 0; i < L->length; i++)
{
L->data[i] = p[i];
}
//[4]修改顺序表的最大长度,其值+len
L->maxsize += len;
//[5]释放原来的存储空间
free(p);
//[6]成功返回true
return true;
}
/*-----------------------------------------------------------*/
/*额外操作——顺序表的输出*/
//<1>静态顺序表的输出
bool SqListPrintf(SqList L)
{
//[1]判空
if (L.length == 0)
{
printf("This sequence list is empty\n");
return false;
}
//[2]输出
printf("SqList:\n");
for (int i = 0; i < L.length; i++)
{
printf("%d ", L.data[i]);
}
printf("end\n");
return true;
}
//<2>动态顺序表的输出
bool SeqListPrintf(SeqList L)
{
//[1]判空
if (L.length == 0)
{
printf("This sequence list is empty\n");
return false;
}
//[2]输出
printf("SeqList:\n");
for (int i = 0; i < L.length; i++)
{
printf("%d ", L.data[i]);
}
printf("end\n");
return true;
}
[1]初始化操作
静态分配和动态分配的顺序表的初始化操作是不同的;算法思路:
【静态分配】在声明一个顺序表时就已经为其分配了数组空间,因此初始化只需将顺序表的当前长度设为0
;
【动态分配】的初始化为顺序表分配一个预定义大小的数组空间,并将顺序表的当前长度设为0
;
【动态分配】MaxSize指示顺序表当前分配的存储空间大小,一旦因插入元素而空间不足,就进行再分配。
//静态分配
//SqList L; //声明一个顺序表
void InitList(SqList &L)
{
L.length=0; //顺序表初始长度为0;
}
/*----------------------------------------------*/
//动态分配
void InitList(SeqList &L)
{
L.data = (ElemType*)malloc(sizeof(ElemType)*MaxSize);//分配存储空间;
L.length = 0; //顺序表初始长度为0;
L.MaxSize = InitSize; //初始存储容量;
}
[2]插入操作
算法思路:
2. 判断i
的范围是否有效;
2. 将第i
个元素及以后的元素后移;
3.在位置i
处放入e
;
4.线性表长度加1
;
5.操作成功,返回true
;时间复杂度分析
最好情况:在表尾插入(即 i = n + 1 i=n+1 i=n+1),元素后移语句将不执行,时间复杂度为 O ( 1 ) O(1) O(1);
最坏情况:在表头插入(即 i = 1 i=1 i=1),元素后移语句将执行n
次,时间复杂度为 O ( n ) O(n) O(n);
平均情况:假设 p i ( p i = 1 / ( n + 1 ) ) p_i(p_i=1/(n+1)) pi(pi=1/(n+1))是在第i
个位置上插入一个结点的概率,则在长度为n
的线性表中插入一个结点时,所需移动结点的平均次数为 ∑ i = 1 n + 1 p 1 ( n − i + 1 ) = ∑ i = 1 n + 1 1 n + 1 ( n − i + 1 ) = 1 n + 1 ∑ i = 1 n + 1 ( n − i + 1 ) = 1 n + 1 n ( n + 1 ) 2 = n 2 \sum_{i=1}^{n+1}p_1(n-i+1)=\sum_{i=1}^{n+1}\frac{1}{n+1}(n-i+1)=\frac{1}{n+1}\sum_{i=1}^{n+1}(n-i+1)=\frac{1}{n+1}\frac{n(n+1)}{2}=\frac{n}{2} i=1∑n+1p1(n−i+1)=i=1∑n+1n+11(n−i+1)=n+11i=1∑n+1(n−i+1)=n+112n(n+1)=2n因此,顺序表插入算法的平均时间复杂度为 O ( n ) O(n) O(n).
bool ListInsert(SqList &L, int i, ElemType e)
{
if(i < 1||i > L.length + 1)
{
return false;
}
if(L.length >= MaxSize)
{
return false;
}
for(int j = L.length; j >= i;j--)
{
L.data[j] = L.data[i-1];
}
L.data[i-1] = e;
L.length++;
return true;
}
[3]删除操作
算法思路:
2. 判断i
的范围是否有效;
2. 将被删除的元素赋值给e
;
3.将第i
个位置后的元素前移;
4.线性表长度减1
;
5.操作成功,返回true
;时间复杂度分析
最好情况:删除表尾元素(即 i = n i=n i=n),无需移动元素,时间复杂度为 O ( 1 ) O(1) O(1);
最坏情况:删除表头元素(即 i = 1 i=1 i=1),需移动除表头元素外的所有元素,时间复杂度为 O ( n ) O(n) O(n);
平均情况:假设 p i ( p i = 1 / ( n ) ) p_i(p_i=1/(n)) pi(pi=1/(n))是删除第i
个位置上结点的概率,则在长度为n
的线性表中删除一个结点时,所需移动结点的平均次数为 ∑ i = 1 n p 1 ( n − i ) = ∑ i = 1 n 1 n ( n − i ) = 1 n ∑ i = 1 n ( n − i ) = 1 n n ( n − 1 ) 2 = n − 1 2 \sum_{i=1}^{n}p_1(n-i)=\sum_{i=1}^{n}\frac{1}{n}(n-i)=\frac{1}{n}\sum_{i=1}^{n}(n-i)=\frac{1}{n}\frac{n(n-1)}{2}=\frac{n-1}{2} i=1∑np1(n−i)=i=1∑nn1(n−i)=n1i=1∑n(n−i)=n12n(n−1)=2n−1因此,顺序表删除算法的平均时间复杂度为 O ( n ) O(n) O(n).可见顺序表中删除与插入操作的时间主要耗费在移动元素上,而移动的元素的个数取决于插入和删除元素的位置。
bool ListDelete(SqList &L, int i, ElemType e)
{
if(i < 1||i > L.length)
{
return false;
}
e = L.data[i - 1];
for(int j = i;j <= L.length; j++)
{
L.data[ j - 1 ] = L.data[j];
}
L.length--;
return true;
}
[4]按值查找(顺序查找)
算法思路:(在顺序表中擦好找一个元素值等于
e
的元素,并返回其位序)时间复杂度分析
最好情况:查找的元素就在表头,仅需比较一次,时间复杂度为 O ( 1 ) O(1) O(1);
最坏情况:查找的元素在表尾(或不存在),需要比较n
次,时间复杂度为 O ( n ) O(n) O(n);
平均情况:假设 p i ( p i = 1 / ( n ) ) p_i(p_i=1/(n)) pi(pi=1/(n))是查找的元素在第i
( 1 < = i < = L . l e n g t h ) (1<=i<=L.length) (1<=i<=L.length)个位置上结点的概率,则在长度为n
的线性表中查找值为e
的元素所需比较的平均次数为 ∑ i = 1 n p 1 ∗ i = ∑ i = 1 n 1 n ∗ i = 1 n n ( n + 1 ) 2 = n + 1 2 \sum_{i=1}^{n}p_1*i=\sum_{i=1}^{n}\frac{1}{n}*i=\frac{1}{n}\frac{n(n+1)}{2}=\frac{n+1}{2} i=1∑np1∗i=i=1∑nn1∗i=n12n(n+1)=2n+1因此,顺序表按值查找算法的平均时间复杂度为 O ( n ) O(n) O(n).顺序表按位查找即根据数组下标访问数组元素,其时间复杂度为 O ( 1 ) O(1) O(1)
2.3顺序表的链式表示(链表)
链式存储线性表时不需要使用地址连续的存储单元,既不要求逻辑上相邻的元素在物理位置上也相邻,因此插入和删除操作不需要移动元素,而只需修改指针,但也会失去顺序表可随机存取的优点。
对每个链表结点,除存放元素自身的信息之外,还需要存放一个指向其后继的指针。
data
为数据域,存放数据元素;next
为指针域,存放其后继结点的地址。
data next
- 通常用头指针
L
(或head
等)来标识一个单链表,指出链表的起始地址,头指针为NULL时表示一个空表。- 同时在单链表的第一个数据结点之前附加一个结点,称为头结点;头结点的数据域可不设任何信息,但也可以记录表长等信息。
- 单链表带头结点时,头指针指向头结点;单链表不带头结点时,头指针指向第一个数据结点。
- 表尾结点的指针域为
NULL
;
头指针和头结点的关系:不管带不带头结点,头指针都指向链表的第一个结点,而头结点是带头结点的链表中的第一个结点,结点内通常不存储信息;
引入头结点的优点:
- 由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无需进行特殊处理;
- 无论链表是否为空,其头指针都是指向头节点的非空指针【不是野指针】(空表中头结点的指针域为空)。因此空表和非空表的处理也得到了统一;
2.3.1 单链表的预编译及数据结构体创建
//[1]必备头文件即预编译部分
#include<stdio.h>
#include<stdlib.h>
//[2]宏定义部分
#define bool char
#define true 1
#define false 0
//[3]自定义数据元素类型
typedef int ElemType;
//[4]单链表的结构体
typedef struct LNode
{
ElemType data;
struct LNode* next;
}LNode,*LinkList;
2.3.2 单链表的基本操作及具体实现函数
初始化、单链表的打印、头插法、尾插法、按位查找元素、按值查找元素、按位插入、按位删除、销毁表
//[5]链表的初始化
bool InitLinkList(LinkList L);//第一种方法
LNode* InitLinkList_1();//第二种方法
//[6]单链表的打印
bool PrintLinkList(LinkList L);
//[7]单链表的头插法
bool HeaderInsertLinkList(LinkList L);
//[8]单链表的尾插法
bool TailInsertLinkList(LinkList L);
//[9]单链表的按位查找数据元素
LNode* GetElem(LinkList L, int i);
//[10]按值查找数据元素
LNode* LocateElem(LinkList L, ElemType e);
//[11]单链表的按位插入
bool LocalInsertElem(LinkList L, int i, ElemType e);
//[12]单链表的按位删除
bool LocalDeletElem(LinkList L, int i);
//[13]销毁单链表
bool DestoryLinkList(LinkList L);
具体操作函数
//[5]链表的初始化
bool InitLinkList(LinkList L)
{
L->data = 0;//头节点的数据域用于存储表长
L->next = NULL;//头节点的next域用来指向链表的第一个节点
return true;
}
LNode* InitLinkList_1()//第二种方法
{
LNode* L = (LNode*)malloc(sizeof(LNode));
L->data = 0;//头节点的数据域用于存储表长
L->next = NULL;//头节点的next域用来指向链表的第一个节点
return L;
}
//[7]单链表的头插法
bool HeaderInsertLinkList(LinkList L)
{
LNode* NewNode; //新节点
ElemType NewNode_data; //新节点的数据域
printf("Please enter a number(999999 means the end):");
scanf("%d", &NewNode_data);
while (NewNode_data != 999999)
{
//进行插入操作
NewNode = (LNode*)malloc(sizeof(LNode));
NewNode->next = L->next;
L->next = NewNode;
NewNode->data = NewNode_data;
L->data++;
printf("Please enter a number(999999 means the end):");
scanf("%d", &NewNode_data);
}
return true;
}
//[8]单链表的尾插法
bool TailInsertLinkList(LinkList L)
{
LNode* NewNode;//指向新节点的指针;
LNode* TailNode = L;//指向尾节点的指针;
ElemType NewNode_data;
while (TailNode->next != NULL)
{
TailNode = TailNode->next;
}
printf("Please enter a number(999999 means the end):");
scanf("%d", &NewNode_data);
while (NewNode_data != 999999)
{
NewNode = (LNode*)malloc(sizeof(LNode));
NewNode->data = NewNode_data;
TailNode->next = NewNode;
NewNode->next = NULL;
TailNode = NewNode;
L->data++;
printf("Please enter a number(999999 means the end):");
scanf("%d", &NewNode_data);
}
return true;
}
//[9]单链表的按位查找数据元素
LNode* GetElem(LinkList L, int i)
{
//[1]判断i的合法性
if (i == 0)
{
printf("The LinkList's element you are looking for does not exist!\nReturn the head pointer of the LinkList!\n");
return L;
}
if (i<1 || i>L->data)
{
printf("The LinkList's element you are looking for does not exist!\nReturn the head pointer of the LinkList!\n");
return NULL;
}
//[2]查找数据元素
LNode* p = L;
for (int j = 0; j < i; j++)
{
p = p->next;
}
return p;
}
//[10]按值查找数据元素
LNode* LocateElem(LinkList L, ElemType e)
{
if (!L->next)
{
printf("This LinkList is empty!\n");
return L;
}
LNode* p = L;
while (p->next)
{
p = p->next;
if (p->data == 0)
{
return p;
}
}
printf("This LinkList's element you are looking for does not exist!\n");
return NULL;
}
//[11]单链表的按位插入
bool LocalInsertElem(LinkList L, int i, ElemType e)
{
//[1]判断i的合法性
if (i<1 || i>(L->data + 1))
{
printf("The position of the element to be inserted is invalid!\n");
return false;
}
//插入新元素
LNode* p = GetElem(L, i - 1);
LNode* NewNode = (LNode*)malloc(sizeof(LNode));
NewNode->data = e;
NewNode->next = p->next;
p->next = NewNode;
L->data++;
return true;
}
//[12]单链表的按位删除
bool LocalDeletElem(LinkList L, int i)
{
//[1]检查i的合法性
if (!L->next)
{
printf("The LinkList is empty!\n");
return false;
}
if (i<1 || i>L->data)
{
printf("The position of the element to be inserted is invalid!\n");
return false;
}
//[2]删除指定元素
LNode* p = GetElem(L, i - 1);
LNode* q = p->next;
p->next = q->next;
free(q);
L->data--;
return true;
}
//[13]销毁单链表
bool DestoryLinkList(LinkList L)
{
while (L->data)
{
LocalDeletElem(L, 1);
PrintLinkList(L);
}
free(L);
return true;
}
/*-------------------------------------------------------------------*/
//额外操作-----------------[6]单链表的打印
bool PrintLinkList(LinkList L)
{
LNode* p;
p = L;
while (p->next)
{
p = p->next;
printf("%d-->", p->data);
}
printf("NULL\n");
return true;
}
2.3.3 单链表的基本操作函数分析
- 单链表的初始化
带头结点和不带头结点的单链表的初始化操作是不同的。带头结点的单链表初始化时,需要创建一个头结点,并让头指针指向头节点,头结点的
next
初始化为NULL
;
bool InitList(LinkList &L)
{
L=(LNode*)malloc(sizeof(LNode));
L->next=NULL:
return true;
}
不带头结点的单链表初始化时,只需将头指针初始化为
NULL
;
bool InitList(LinkList &L)
{
L=NULL;
return true;
}
- 求表长操作
算法思路:从第一个结点开始依次访问表中每个结点,为此需设置一个计数变量,每访问一个节点,其值加一,知道访问到空结点为止;
int Length(LinkList L)
{
int len=0;
LNode* p=L;
while(p->next!=NULL)
{
p=p->next;
len++;
}
return len;
}
求表长操作的时间复杂度为 O ( n ) O(n) O(n)。注意单链表的长度不包括头结点。
- 按序号查找结点
算法思路:从链表的第一个结点开始,沿着next
域从前往后依次搜索,直到找到第i
个结点为止,则返回该节点的指针;若i
小于单链表的表长,则返回NULL
;
LNode *GetElem(LinkList L,int i)
{
LNode *p=L;
int j=0;
while(p!=NULL&&j<i)
{
p=p->next;
j++;
}
return p;
}
按序号查找操作的时间复杂度为 O ( n ) O(n) O(n).
- 按值查找表结点
从链表的第一个节点开始,从前往后依次比较表中各结点的数据域,若某结点的data
域等于给定值e
,则返回该节点的指针;若整个单链表表中没有这样的结点,则返回NULL
;
LNode *LocateElem(LinkList L,ElemType e)
{
LNode *p=L->next;
while(p!=NULL&&p->data!=e)
{
p=p->next;
}
return p;
}
按值查找操作的时间复杂度为 O ( n ) O(n) O(n);
- 插入结点操作
算法思路:插入结点操作将值为x
的新结点插入到单链表的第i
个位置。先检查插入位置的合法性,然后找到待插入位置的前驱,即第i-1
个结点,再在其后插入。
[首先查找i-1
个节点,假设第i-1
个结点为*p
,然后令新结点*s
的指针域指向*p
的后继,再令结点*p
的指针域指向新插入的结点*s
。]
bool ListInsert(LinkList &L,int i,ElemType e){
LNode *p=L;
int j=0;
while(p!=NULL&&j<i-1)
{
p=p->next;
j++;
}
if(p==NULL)
{
return false;
}
LNode* s=(LNode*)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;//步骤一
p->next=s;//步骤二
return true;
}
插入时,操作一和二的顺序不能颠倒;
本算法主要时间开销在于查找i-1
个元素,时间复杂度为O(n)
,若在指定结点后插入新结点,则时间复杂度仅为O(1)
;
注意:当链表不带头结点时,需要判断插入位置i
是否为1
,若是,则要做特殊处理,将头指针L指向新的首结点;当链表带头结点时,插入位置i
为i
时不用做特殊处理。
- 扩展:对某一结点进行前插操作
前插操作是指在某结点的前面插入一个新结点,后插操作的定义刚好与之相反。在单链表插入算法中,通常都采用后插操作。
以上面的算法为例,先找到第i-1
个结点,即插入结点的前驱,再对其执行后插操作。由此可知,对结点的前插操作均可转化为后插操作,前提是从单链表的头结点开始顺序查找到其前驱结点,时间复杂度为O(n)。
此外,可采用另一种方式将其转化为后插操作来实现,设待插入结点为*s
,将*s
插入到*p
的前面。我们仍然将*s
插入到*p
的后面,然后将p->data
与s->data
交换,这样做既满足逻辑关系,又能使得时间复杂度为0(1)。该方法的主要代码片段如下:
s->next=p->next;
p->next=s;
temp=p->data;
p->data=s->data;
s->data=temp;
- 删除结点操作
算法思路:先检查删除位置的合法性,然后查找表中i-1
个结点,即被删结点的前驱,再删除第i
个结点。
假设节点*p
为找到的被删结点的前驱,为实现这一操作后的逻辑关系的变化,仅需修改*p
的指针域,将*p
的指针域next
指向*q
的下一结点,然后释放*q
的存储空间。
bool ListDelete(LinkList &L,int i,ElemType &e)
{
LNode *p=L;//指针p指向当前扫描到的结点;
int j=0;//记录当前节点位序,头结点是第0个结点;
while(p!=NUlLL&&j<i-1)//循环找到第i-1个结点;
{
p=p->next;
j++;
}
if(p==NULL||p->next==NULL)//i值不合法
{
return false;
}
LNode *q=p->next;//令q指向被删除结点;
e=q->data;//用e返回元素的值;
p->next=q->next;//将*q结点从链中“断开”
free(q);//释放
return true;
}
同插入算法一样,该算法的主要时间也耗费在查找操作上,时间复杂度为 O ( n ) O(n) O(n)。
当链表不带头结点时,需要判断被删结点是否为首结点,若是,则要做特殊处理,将头指针工指向新的首结点。当链表带头结点时,删除首结点和删除其他结点的操作是相同的。
- 扩展:删除结点p。
要删除某个给定结点p,通常的做法是先从链表的头结点开始顺序找到其前驱,然后执行删除操作。
其实,删除结点p的操作可用删除p的后继来实现,实质就是将其后继的值赋予其自身,然后再删除后继,也能使得时间复杂度为0(1)。该方法的主要代码片段如下:
q=p->next;
p->data=p->next->data;
p->next=q->next;
free(q);
- 采用头插法建立单链表
算法思路:该算法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,及头结点之后。
LinkList List_HeadInsert(LinkList &L)
{
LNode* s;
int x;
L=(LNode*)malloc(sizeof(LNode));
L->next=NULL:
scanf("%d",&x);
while(x!=9999)
{
s=(LNode*)malloc(sizeof(LNode));
s->data=x;
s->next=L->next;
L->next=s;
scanf("%d",&x);
}
return L;
}
采用头插法创建单链表时,读入数据的顺序和生成的链表中元素的顺序是相反的,可用来实现链表的逆置。每个节点插入的时间为 O ( 1 ) O(1) O(1),设单链表表长为
n
,则总时间复杂度为 O ( n ) O(n) O(n).
8. 采用尾插法建立单链表
算法思路:该方法将新结点插入到当前链表的表尾,为此必须增加一个尾指针r
,使其始终指向当前链表的尾结点;
LinkList List_TailInsert(LinkList &L)
{
int x;
L=(LNode*)malloc(sizeof(LNode));
LNode* s;
*r=L;
scanf("%d",&x);
while(x!=9999)
{
s=(LNode*)malloc(sizeof(LNode));
s->data=x;
r->next=s;
r=s;//r指向新的表尾结点
scanf("%d",&x);
}
r->next=NULL;
return L;
}
算法分析:时间复杂度与头插法相同;
2.3.4 双链表
- 双链表的定义:
双链表结点中有两个指针prior
和next
,分别指向其直接前驱和直接后驱,表头结点的prior
和尾结点的next
都是NULL;
双链表在单链表结点中增加了一个指向其前驱的指针prior
,因此双链表的按值查找和按位查找的操作与单链表的相同;
双链表的建立同样可参考头插法和尾插法;
但双链表再插入和删除操作的实现上,其时间复杂度仅为 O ( 1 ) O(1) O(1).- 双链表的结点类型:
typedef struct DNode
{
ElemType data;
struct DNode *prior,*next;
}DNode,*DLinkList;
- 双链表的插入操作
具体实现代码如下:
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s
- 双链表的删除操作
具体实现代码如下:
p->next=q->next;
q->next->prior=p;
free(q);
2.3.5 循环链表
- 循环单链表
循环单链表和单链表的区别在于,表中最后一个结点的指针不是NULL
,而改为指向头结点,从而整个链表形成一个环。
在循环单链表中,表尾结点*r
的next
域指向工,故表中没有指针域为NULL
的结点,因此,循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针。- 循环双链表
由循环单链表的定义不难推出循环双链表。不同的是,在循环双链表中,头结点的prior
指针还要指向表尾结点。
当某结点*p
为尾结点时,p->next==L;
当循环双链表为空表时,其头结点的prior
域和next
域都等于L
。2.3.5 静态链表
静态链表是用数组来描述线性表的链式存储结构,结点也有数据域
data
和指针域next
,与前面所讲的链表中的指针不同的是,这里的指针是结点在数组中的相对地址(数组下标),又称游标。
和顺序表一样,静态链表也要预先分配一块连续的内存空间。
静态链表以next==-1
作为其结束的标志;
静态链表数据结构的描述如下:
#define MaxSize 50
typedef struct {
ElemType data;
int next;
}SLinkList[MaxSize];
3.栈、队列和数组
3.1 栈
3.1.1 栈的基本概念
栈的定义:(后进先出)
栈stack
是只允许在一端进行插入或删除操作的线性表;
栈顶top
:线性表允许进行插入删除的那一端;
栈底bottom
:固定的,不允许插入和删除的另一端;
空栈:不含任何元素的空表;栈的基本操作:
初始化、进栈、出栈、读栈顶元素、销毁栈
//[1]初始化栈
bool InitStack(SqStack* s);
//[2]栈判空
bool StackEmpty(SqStack s);
//[3]进栈(压栈)
bool Push(SqStack* s, Elemtype e);
//[4]出栈
bool Pop(SqStack* s, Elemtype* e);
//[5]读栈顶元素
bool GetTop(SqStack s, Elemtype* e);
栈的数学性质(卡特兰数公式):当
n
个不同元素进栈时,出栈元素的不同排列的个数为 1 n + 1 C 2 n n \frac{1}{n+1}C^{n}_{2n} n+11C2nn。3.1.2 栈的顺序存储结构
栈是线性表,类似的,它也有顺序和链式两种存储方式;
- 顺序栈的实现
采用顺序存储的栈称为顺序栈,它采用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针top
指示当前栈顶元素的位置;
【预编译及必备头文件】
#include<stdio.h>
#include<stdlib.h>
#define bool char
#define true 1
#define false 0
#define MaxSize 50 //定义栈中元素最大个数
typedef int Elemtype;
typedef struct SqStack
{
Elemtype data[MaxSize];
int top;
}SqStack;
- 顺序栈的初始化
栈顶指针:S.top
,初始时设置S.top=-1
;
栈顶元素:S.data[S.top]
。
进栈操作:栈不满时,栈顶指针先加一,再送值到栈顶;
出栈操作:栈非空时,先取栈顶元素,再将栈顶指针减一;
栈空条件:S.top==-1
;
栈满条件:S.top==MaxSize-1
;
栈长:S.top+1
。
//[1]初始化栈
bool InitStack(SqStack* s)
{
s->top = -1;
}
- 另一种常见定义方式:
初始化设置栈顶指针S.top=0
;进栈时先将值送到栈顶,栈顶指针加一;出栈时,栈顶指针先减一,再取栈顶元素;栈空条件是S.top==0
;栈满条件是S.top==MaxSize
;
- 栈判空:
bool StackEmpty(SqStack S)
{
if(S.top==-1)//栈空
{
return true;
}
else
return false;
}
- 进栈:
bool Push(SqStack* s,Elemtype e)
{
//判断栈满
if (s->top == MaxSize - 1)
{
return false;
}
//指针先加一,再入栈
s->data[++s->top] = e;
return true;
}
- 出栈:
bool Pop(SqStack* s, Elemtype *e)
{
//判空
if (StackEmpty(*s))
{
return false;
}
//先出栈,指针再减一
*e = s->data[s->top--];
return true;
}
- 读栈顶元素:
仅为读取栈顶元素,并无出栈操作;
bool GetTop(SqStack s, Elemtype* e)
{
if (StackEmpty(s))
{
return false;
}
*e = s.data[s.top];
return true;
}
- 共享栈
利用栈底位置相对不变的特性,可让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。
两个栈的栈顶指针都指向栈顶元素,top0=-1
时0号栈
为空,top1=Maxsize
时1号栈
为空;仅当两个栈顶指针相邻(top1-top0=1)
时,判断为栈满。
当0号栈
进栈时top0
先加1
再赋值,1号栈
进栈时top1
先减1
再赋值;出栈时则刚好相反。
共享栈是为了更有效地利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢。其存取数据的时间复杂度均为 O ( 1 ) O(1) O(1),所以对存取效率没有什么影响。3.1.3 栈的链式存储
采用链式存储的栈称为链栈,链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。
通常采用单链表实现,并规定所有操作都是在单链表的表头进行的。这里规定链栈没有头结点,Lhead
指向栈顶元素。
typedef struct LinkList
{
ElemType data;
struct Linknode *next;
}Linkstack;
3.2 队列
3.2.1 队列的基本概念
- 队列的定义
队列(Queue)
简称队,也是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。
向队列中插入元素称为入队或进队;删除元素称为出队或离队。
其操作的特性是先进先出(First InFirst Out,FIFO)
。
队头Front
。允许删除的一端,又称队首;
队尾Rear
。允许插入的一端;
空队列。不含任何元素的空表;- 队列的基本操作;
初始化Init
、判空Empty
、入列En
、出列De
、读队头元素GetHead
;
注意:不可以随便读取栈或队列中间的某个数据。3.2.2 队列的顺序存储结构
- 队列的顺序存储:
队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针:队头指针 front 指向队头元素,队尾指针rear 指向队尾元素的下一个位置。
队列的顺序存储类型可描述为:
#define MaxSize 50
typedef struct {
ElemType data[MaxSIze];
int front,rear;
}SqQueue;
初始时:
Q.front=Q.rear=0
.
进队操作:队不满时,先送值到队尾元素,再将队尾指针加1
。
出队操作:队不空时,先取队头元素值,再将队头指针加1
。
队列的初始状态,有Q.front==Q.rear==0
成立,该条件可以作为队列判空的条件。但能否用Q.rear==Maxsize
作为队列满的条件呢?显然不能,队列中仅有一个元素,但仍满足该条件。这时入队出现“上溢出”,但这种溢出并不是真正的溢出,在data
数组中依然存在可以存放元素的空位置,所以是一种“假溢出”。
- 循环队列
上面指出了顺序队列“假溢出”的问题,这里引出循环队列的概念。
将顺序队列臆造为一个环状的空间,即把存储队列元素的表从逻辑上视为一个环,称为循环队列。
当队首指针Q.front=Maxsize-1
后,再前进一个位置就自动到0
,这可以利用除法取余运算($)
来实现。
初始时:Q.front=Q.rear=0
。
队首指针进1:Q.front=(Q.front+1)&Maxsize
。
队尾指针进1:Q.rear=(Q.rear+1)%Maxsize
。
队列长度:(Q.rear+MaxSize-Q.front)%Maxsize
。
出队入队时:指针都按顺时针方向进1
。