准备25考研,参考了王道和几位博主,自用。
一、线性表的定义
1.线性表的定义和基本操作
①线性表的定义
线性表是相同数据类型的n个元素的有限序列。只有一个直接前驱和一个直接后继。
②线性表的基本操作
操作 | 描述 |
---|---|
InitList(&L) | 初始化表。构造一个空的线性表L,分配内存空间 |
DestroyList(&L) | 销毁操作。销毁线性表,并释放线性表L所占用的内存空间 |
ListInsert(&L,i,e) | 插入操作。在表L中的第i个位置上插入指定元素e |
ListDelete(&L,i,&e) | 删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值 |
LocateElem(L,e) | 按值查找操作。在表L中查找具有给定关键字值的元素 |
GetElem(L,i) | 按位查找操作。获取表L中第i个位置的元素的值 |
Length(L) | 求表长。返回线性表L的长度,即L中数据元素的值 |
PrintList(L) | 输出操作。按前后顺序输出线性表L的所有元素值 |
Empty(L) | 判空操作。若L为空表,则返回true,否则返回false |
概括就是创建,销毁,判空,清空,计数,插入,删除,查找(按位与按值),增加
"&“是C++的用法,表示引用。是否要传入引用”&“取决于对参数的修改结果是否需要"带回来”
2.顺序表的定义(重要!!!)
①顺序表的定义
顺序表是存储结构上使用顺序存储。
②顺序表的实现
(i)静态分配
定义数组
#define MAX_SIZE 100
typedef struct complex {
Elemtype data[MAX_SIZE];
int length;
}SQList;
(ii)动态分配
因为静态分配实现的顺序表的表长开始确定后就无法更改(存储空间是静态的),刚开始就声明一个很大的内存空间又很浪费。所以可以用动态分配的方式实现顺序表大小可变
typedef struct complex {
Elemtype *data;
int length,MAX_SIZE;
}SQList;
使用 L.data=(Elemtype *)malloc(sizeof(Elemtype)*size); 动态分配。
③顺序表的特点
④顺序表的操作
1)顺序表的定义
静态分配(数组)
#include<stdio.h>
#define MaxSize 10 //定义表的最大长度
typedef struct{
int data[MaxSize];//用静态的"数组"存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义(静态分配方式)
void InitList(SqList &L)
{
for(int i=0;i<MaxSize;i++){
L.data[i]=0; //将所有数据元素设置为默认初始值
}
L.length=0;
}
int main()
{
SqList L; //声明一个顺序表
InitList(L); //初始化一个顺序表
for(int i=0;i<MaxSize;i++){
printf("data[%d]=%d\n",i,L.data[i]);
}
return 0;
}
动态分配(指针)
//顺序表的实现——动态分配
#include<stdio.h>
#include<stdlib.h> //malloc、free函数的头文件
#define InitSize 10 //默认的最大长度
typedef struct
{
Elemtype *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList;
//初始化
void InitList(SeqList &L)
{
//用malloc 函数申请一片连续的存储空间
L.data =(int*)malloc(InitSize*sizeof(int)) ;
L.length=0;
L.MaxSize=InitSize;
}
//增加动态数组的长度
void IncreaseSize(SeqList &L,int len)
{
//realloc增加数组长度
/*
int *temp = NULL;
temp = (int *)realloc(list->elements,
sizeof(int)*(list->size + len));
if (temp == NULL) {
perror("realloc failed");
exit(EXIT_FAILURE);
}
list.elements = temp;
list.MaxSize+= len;
*/
int *temp=L.data;
L.data=(int*)malloc((L.MaxSize+len)*sizeof(int));
for(int i=0;i<L.length;i++){
L.data[i]=temp[i]; //将数据复制到新区域
}
L.MaxSize=L.MaxSize+len; //顺序表最大长度增加len
free(temp); //释放原来的内存空间
}
int main(void)
{
SeqList L; //声明一个顺序表
InitList(L); //初始化顺序表
IncreaseSize(L,5);
return 0;
}
2)顺序表的插入
ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。
平均时间复杂度 = O(n)。
注意!!!!!!!!!!!!!!!
插入位置是可以从0~n+1的说其为插入位置时说的是位置(从1开始),而不是数组下标(从0开始)。同时其移动元素是n-i+1。
插入操作中当内存空间已满,先申请n+m个空间,再将n个数据复制过去,最后删除对应n的原来空间。(遵循连续存放的地址空间)
#define MaxSize 10 //定义最大长度
typedef struct
{
Elemtype data[MaxSize]; //用静态的数组存放数据
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
bool ListInsert(SqList &L, int i, int e)
{
if(i<1||i>L.length+1) //判断i的范围是否有效
return false;
if(L.length>MaxSize) //当前存储空间已满,不能插入
return false;
for(int j=L.length; j>=i; j--){ //将第i个元素及其之后的元素后移
L.data[j]=L.data[j-1];
}
L.data[i-1]=e; //在位置i处放入e
L.length++; //长度加1
return true;
}
int main()
{
SqList L; //声明一个顺序表
InitList(L);//初始化顺序表
//...此处省略一些代码;插入几个元素
ListInsert(L,3,3); //再顺序表L的第三个位置插入3
return 0;
}
要把第i个位置开始的n-i+1个元素全部后移一位,再在第i个位置上填入要插入的元素e
3)顺序表的删除
ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
平均时间复杂度 = O(n)。
注意!!!!!!!!!!!!!!!
删除的位置是从1~n。同时删除元素移动了n-i个元素。
bool LisDelete(SqList &L, int i, int &e)
{
if(i<1||i>L.length) //判断i的范围是否有效
return false;
e = L.data[i-1] //将被删除的元素赋值给e
for(int j=L.length; j>=i; j--)
{
L.data[j-1]=L.data[j]; //将第i个后的元素前移
}
L.length--; //长度减1
return true;
}
int main()
{
SqList L; //声明一个顺序表
InitList(L);//初始化顺序表
//...此处省略一些代码;插入几个元素
int e = -1; //用变量e把删除的元素带回来
if(ListDelete(L,3,e))
{
printf("已经删除第3个元素,删除元素值为=%d\n",e);
}
else
{
printf("为序i不合法,删除失败\n");
}
return 0;
}
4)顺序表的查找
顺序表的按位查找
GetElem(L,i) 按位查找操作。获取表L中第i个位置的元素的值
不管是采用静态分配的还是动态分配,都可以用如下方式实现按位查找
ElemType GetElem(SqList L,int i){
return L.data[i-1];
}
只有一个return语句,没有递归调用也没有循环,时间复杂度为 O(1)。
由于顺序表的各个数据元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第i个元素。这就是顺序表的“随机存取”特性。
顺序表的按值查找
LocateElem(L,e) 按值查找操作。在表L中查找具有给定关键字值的元素
平均时间复杂度为O(n)。
#define InitSize 10 //定义最大长度
typedef struct
{
ElemTyp *data; //用静态的“数组”存放数据元素
int Length; //顺序表的当前长度
}SqList;
//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateElem(SqList L, ElemType e)
{
for(int i=0; i<L.lengthl i++)
if(L.data[i] == e)
return i+1; //数组下标为i的元素值等于e,返回其位序i+1
return 0; //推出循环,说明查找失败
}
//调用LocateElem(L,9)
5)线性表的排序
利用第八章的排序算法进行描述
。。。有机会再写
6)线性表的逆
Elemtype List_Reverse(SqList L)
{
int x = L->leng-1;
Elemtype temp;
for (int i = 0;i<(L->length)/2;i++,x--)
{
temp = L->data[i];
L->data[i]= L->data[x];
L->data[x] = temp;
}
return Lb;
}
7)线性表的合并
void Merge_List(SeqList &L1, SeqList &L2)
{
// 确保 L1 有足够的空间来存储合并后的数据
if (L1.MaxSize < L1.length + L2.length) {
L1.data = (int*)realloc(L1.data, (L1.length + L2.length) * sizeof(int));
L1.MaxSize = L1.length + L2.length;
}
// 将 L2 的数据复制到 L1
for (int i = 0; i < L2.length; i++) {
L1.data[L1.length + i] = L2.data[i];
}
// 更新 L1 的长度
L1.length += L2.length;
// 释放 L2 的数据空间(如果不再使用的话)
free(L2.data);
L2.data = NULL;
L2.length = 0;
L2.MaxSize = 0;
}
8)线性表的拆分
// 拆分线性表
void List_Split(SeqList &L, SeqList &L1, SeqList &L2, int splitPoint) {
// 确保拆分点合法
if (splitPoint < 0 || splitPoint > L.length) {
printf("Invalid split point.\n");
return;
}
// 初始化 L1 和 L2
InitList(L1, splitPoint); // L1 的最大容量为拆分点
InitList(L2, L.length - splitPoint); // L2 的最大容量为剩余部分
// 复制数据到 L1
for (int i = 0; i < splitPoint; i++) {
L1.data[i] = L.data[i];
}
L1.length = splitPoint;
// 复制数据到 L2
for (int i = splitPoint; i < L.length; i++) {
L2.data[i - splitPoint] = L.data[i];
}
L2.length = L.length - splitPoint;
}
3.单链表的定义(重要!!!!)
这一节如果对代码的逻辑不清楚可以采用画图表示指针指向的方式来清晰思路
①单链表的创建操作
什么是单链表
采用这种存储方式要找到某一个位序的结点,只能从第一个结点开始利用指针的信息依次往后寻找直到找到我们想要的那个结点,因此单链表这种实现方式不支持随机存取。
1)代码定义
typedef struct LNode{ //定义单链表结点类型
ElemType data; //每个节点存放一个数据元素
struct LNode *next; //指针指向下一个节点
}LNode,*LinkList;
//强调这是一个单链表使用LinkList,强调这是一个结点用LNode*,实际上LinkList和LNode*是等价的
2)不带头节点:
typedef struct LNode{ //定义单链表结点类型
ElemType data; //每个节点存放一个数据元素
struct LNode *next; //指针指向下一个节点
}LNode,*LinkList;
//初始化一个空的单链表
bool Init_List(LinkList &L){
L=NULL; //空表,暂时还没有结点,防止脏数据
return true;
}
void test(){
LinkList L; //声明一个指向单链表的指针,注意此处并没有创建一个结点
//初始化一个空表
InitList(L);
//......后续代码......
}
3)带头节点
typedef struct LNode{ //定义单链表结点类型
ElemType data; //每个节点存放一个数据元素
struct LNode *next; //指针指向下一个节点
}LNode,*LinkList;
//初始化一个空的单链表
bool Init_List(LinkList &L){
L=(LNode *)malloc(sizeof(LNode)); //分配一个头结点
if(L==NULL) //内存不足,分配失败
return false;
L->next=NULL; //头结点不存储数据,头结点之后暂时没有结点
return true;
}
void test(){
LinkList L; //声明一个指向单链表的指针
//初始化一个空表
InitList(L);
//......后续代码......
}
②单链表的判空
bool List_isEmpty(LinkList L){
return(L==NULL);
}
③单链表的插入
1)按位序插入(带头结点)
List_Insert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e
-
这里的i指的是位序,不包括头结点,从1开始计数
-
思路是找到第i-1个结点,将新结点插入其后,这里就体现了带头结点的好处,可以把头结点视为第0个结点进行处理
插入的位置越小时间复杂度越小,比如在第1个位置上插入新结点为最好时间复杂度,为O (1) 。最坏时间度为在表尾插入新的结点,时间复杂度为O(n)。平均时间复杂度为O(n)。
//在第i个位置插入元素e(带头结点)
bool List_Insert(LinkList &L,int index,ElemType e){
if(index<1)
return false;
LNode* p; //指针p指向当前扫描到的结点
int i=0; //当前p指向的是第几个结点
p=L; //L指向头结点,头结点是第0个结点(不存数据)
while(p!=NULL && i<index-1){ //循环找到第i-1个结点
p=p->next;
j++;
}
if(p==NULL) //index值不合法
return false;
LNode* node=(LNode*)malloc(sizeof(LNode));
node->data=data;
node->next=p->next;
p->next=node; //将结点node连到p之后
return true; //插入成功
}
2)按位序插入(不带头结点)
- 不存在第0个结点,所以i=1要特殊处理。因为如果插入,删除第1个元素时,需要更改头指针L。后续逻辑和带头结点的一样。
- 不带头结点写代码更不方便,推荐用带头结点。除非特别声明,之后的代码默认带头结点
bool List_Insert(LinkList &L,int index,ElemType data){
if(index<1)
return false;
if(index==1){ //插入第1个结点的操作与其他结点操作不同
LNode* node=(LNode*)malloc(sizeof(LNode));
node->data=data;
node->next=L;
L=node; //头指针指向新结点
return true;
}
LNode* p; //指针p指向当前扫描到的结点
int i=1; //当前p指向的是第几个结点,注意从1开始
p=L; //p指向第1个结点(注意:不是头结点)
while(p!=NULL && i<index-1){ //循环找到第i-1个结点
p=p->next;
j++;
}
if(p==NULL) //index值不合法
return false;
LNode* node=(LNode*)malloc(sizeof(LNode));
node->data=data;
node->next=p->next;
p->next=node; //将结点node连到p之后
return true; //插入成功
}
3)指定结点的后插操作
- 由于单链表的链接指针只能往后寻找,所以如果给定一个结点p的话,p之后的结点都是可知的,可以用循环的方式把它们都找出来,但是p结点之前的那些结点就没法知道了
时间复杂度为O(1)。
//后插操作:在p结点之后插入元素e
bool List_Insert_Behind(LNode *p,ElemType data){
if(p==NULL)
return false;
LNode* node=(LNode *)malloc(sizeof(LNode));
if(node==NULL) //某些情况下有可能分配失败(如内存不足),自己写代码可以不写
return false;
node->data=data; //用结点node保存数据元素e
node->next=p->next;
p->next=node; //将结点node连到p之后
return true;
}
4)指定结点的前插操作
时间复杂度为O(1)。
- 注意这里运用了交换数据的思想
//前插操作:在p结点之前插入元素e
bool List_Insert_Front(LNode *p,ElemType data){
if(p==NULL)
return false;
LNode* node=(LNode *)malloc(sizeof(LNode););
if(node==NULL)
return false; //内存分配失败
node->next=p->next;
p->next=s; //新结点s连到p之后
s->data=p->data; //将p中元素复制到s中
p->data=data; //p中元素覆盖为e
return true;
}
书上则是直接传入了待插入的结点
④单链表的删除
1)按位序删除
List_Delete(&L,i,&e):删除操作。删除表中第i个位置的元素,并用data返回删除的元素
- 思路是找到第i-1个结点,将其指针指向第i+1个结点并释放第i个结点
- 头结点可以看作第0个结点
由于要循环遍历,因此最坏时间复杂度和平均时间复杂度都是O(n),最好时间复杂度为O(1)。
bool List_Delete(LinkList &L,int index,ElemType &data){
if(index<1)
return false;
LNode* p; //指针p指向当前扫描到的结点
int i=0; //当前p指向的是第几个结点
p=L; //L指向头结点,头结点是第0个结点(不存数据)
while(p!=NULL && i<index-1){ //循环 找到第i-1个结点
p=p->next;
i++;
}
if(p==NULL) //i值不合法
return false;
if(p->next==NULL) //第i-1个结点之后已无其他结点
return false;
LNode *node=p->next; //令q指向被删除结点
data=node->data; //用e返回元素的值
p->next=node->next; //将*q结点从链中“断开”
free(node); //释放结点的存储空间
return true; //删除成功
}
2)指定结点的删除
删除结点p,需要修改其前驱结点的next指针
时间复杂度为O(1)。
//删除指定结点p
bool List_Delete_Node(LNode *p){
if(p==NULL){
return false;
}
LNode *node=p->next; //令q指向*p的后继结点
p->data=p->next->data; //和后继结点交换数据域
p->next=node->next; //将*q结点从链中"断开"
free(q); //释放后继结点的存储空间
return true;
}
⑤单链表的查找
按位查找
List_Index_Get(L,index):按位查找操作。获取表L中第index个位置的元素的值
- 其实在上一节的按位插入和按位删除两个基本操作里面已经实现了按位查找的相关代码逻辑,只不过之前找的是第i-1个结点,改成找第i个结点即可
//按位查找,返回第i个元素(带头结点)
LNode* List_Index_Get(LinkList L,int index){
if(index<0)
return NULL;
LNode* p; //指针p指向当前扫描到的结点
int i=0; //当前p指向的是第几个结点
p=L; //L指向头结点,头结点是第0个结点(不存数据)
while(p!=NULL&&i<index){ //循环找到第i个结点
p=p->next;
i++;
}
return p;
}
⑥单链表的逆置
void List_Reverse(LinkList L)
{
Linknode *p;
Linknode *q;
p = L->next; //p指向链表头节点
L->next = NULL; //头节点赋空
while (p)
{
q = p; //将q结点等于p结点,接下来对q结点进行操作,p结点指向其下一结点,用于循环条件的判断
p = p->next;
q->next = L->next;
L->next = q;
}
}
⑦单链表的的排序
⑧单链表的合并
// 合并两个单链表
void Lists_Merge(LinkList L1, LinkList L2, LinkList &L) {
// 初始化合并后的链表
Init_List(L);
LNode *p1 = L1->next; // 指向 L1 的第一个节点
LNode *p2 = L2->next; // 指向 L2 的第一个节点
LNode *p = L; // 合并链表的当前节点
// 合并链表
while (p1 && p2) {
if (p1->data <= p2->data) {
p->next = p1;
p1 = p1->next;
} else {
p->next = p2;
p2 = p2->next;
}
p = p->next;
}
// 处理剩余节点
if (p1) p->next = p1;
if (p2) p->next = p2;
}
⑨单链表的拆分
// 拆分链表
void List_Split(LinkList L, LinkList &L1, LinkList &L2) {
if (L == NULL || L->next == NULL) {
L1 = NULL;
L2 = NULL;
return;
}
// 使用快慢指针找到链表的中点
LNode *slow = L->next;
LNode *fast = L->next;
LNode *prev = NULL;
while (fast != NULL && fast->next != NULL) {
prev = slow;
slow = slow->next;
fast = fast->next->next;
}
//上面使用查找算法找到中点
// 拆分链表
L1 = L;
L2 = (LNode *)malloc(sizeof(LNode)); // 创建第二个链表头结点
L2->next = slow;
prev->next = NULL; // 断开链接,结束第一个链表
// 处理第二个链表
if (L2->next == NULL) {
free(L2); // 如果第二个链表为空,释放内存
L2 = NULL;
}
}
4.双链表
在单链表中每个结点只包含指向它后继结点的指针,所以给定一个结点p的话,我们要找到它的前驱结点是很麻烦的,即无法逆向检索。双链表在单链表的基础上再增加一个指针域指向结点的前驱结点,可进可退,但是存储密度低一些
1)双链表的定义
typedef struct DNode{ //定义双链表结点类型
ElemType data; //数据域
struct DNode *prior,*next; //前驱和后继指针
}DNode,*DLinklist;
2)双链表的初始化(带头结点)
//初始化双链表
bool Init_DLinkList(DLinklist &L){
L=(DNode*)malloc(sizeof(DNode)); //分配一个头结点
if(L==NULL) //内存不足,分配失败
return false;
L->prior=NULL; //头结点的prior永远指向NULL
L->next=NULL; //头结点之后暂时没有结点
return true;
}
//判断双链表是否为空(带头结点)
bool DLinkList_isEmpty(DLinklist L){
if(L->next==NULL)
return true;
else
return false;
}
3)双链表的插入
//在p结点之后插入s结点
bool DLinkList_Insert(DNode *p,DNode *node){
if(p==NULL||node==NULL) //非法参数
return false;
node->next=p->next;
if(p->next!=NULL) //如果p结点有后继结点
p->next->prior=node;
node->prior=p;
p->next=s;
}
时间复杂度为O(1)。
4)双链表的删除
//删除p结点的后继结点
bool DLinkList_Delete_Node(DNode *p){
if(p==NuLL)
return false;
DNode *q=p->next; //找到p的后继结点q
if(q==NULL)
return false; //p没有后继
p->next=q->next;
if(q->next!=NULL) //q结点不是最后一个结点
q->next->prior=p;
free(q); //释放结点恐慌
return true;
}
时间复杂度为O(1)。
5)双链表的遍历
①
while(p!=NULL){
//对结点p做相应处理,如打印
p=p->next;
}
②
//如果只想处理数据结点而不处理头结点,可以跳过头结点
while(p->prior!=NULL){
//对结点p做相应处理,如打印
p=p->prior;
}
while(p!=NULL){
//对结点p做相应处理
p=p->prior;
}
双链表不可随机存取,按位查找和按值查找操作都只能用遍历的方式实现。时间复杂度为 O(n)。
5.循环链表
循环单链表
循环双链表
6.静态链表
虽然静态链表的存储空间是一整片连续存储空间,但是在这一片空间内各个逻辑上相邻的数据元素也可以在物理上不相邻,各个元素之间的逻辑关系通过游标来表示
什么是静态链表。
顺序表和链表的比较
下面分别从逻辑结构,存储结构,基本操作的角度对顺序表和链表进行比较
1)逻辑结构
都属于线性表,都是线性结构
存储结构
基本操作
对于任何一个数据结构,基本操作基本都能归纳为创销,增删改查。其中改建立在查的基础上
创建
销毁
增加与删除
查找
用链表还是顺序表
顺序表 | 链表 | |
---|---|---|
弹性(可扩容) | × | √ |
增,删 | × | √ |
改 | √ | × |
表长难以预估,经常需要增加/删除元素——链表
表长可预估,查询(搜索)操作较多——顺序表