系列索引👉2023考研王道数据结构知识梳理
文章目录
第二章:线性表
1. 线性表
1.1. 线性表定义
线性表是具有相同数据类型的 n ( n ≥ 0 ) n(n\ge0) n(n≥0)个元素的有限序列,其中 n n n为表长,当 n = 0 n=0 n=0时,该表为空表。
- a i a_i ai是线性表中的“第 i i i个”元素线性表中的位序。
- a 1 a_1 a1是表头元素; a n a_n an是表尾元素。
- 除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。
1.2. 线性表的特点
- 表中元素个数有限。
- 表中元素具有逻辑上的顺序性,在序列中各个元素排序有其先后次序。
- 表中元素都是数据元素,每个元素都是单个元素。
- 表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间。
- 表中元素具有抽象性,即讨论元素间一对一的逻辑关系,而不考虑元素究竟表示的内容。
- 线性表是一种逻辑结构,表示元素之间一对一相邻的关系。
1.3. 线性表的基本操作
- 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。
命名要有可读性,最好以上述为标准,否则初始的是否要写上注释。
2. 顺序表
2.1. 顺序表的定义
用顺序存储的方式实现线性表的顺序储存。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
2.2. 顺序表的实现
2.2.1. 顺序表的静态分配
有不明白结构体定义方式的可以看看这篇C 结构体,C与C++的结构题的区别请点击C和Cpp在结构体上的区别(不包括面向对象的区别)
#define MaxSize 10 // 定义最大的长度
typedef struct {
ElemType data[MaxSize]; // ElemType是data数组的类型,用静态的数组来存储数据。
int length; // 顺序表的当前的长度
}SqList; // 顺序表的类型定义(静态分配方式)
完整的程序
// C语言程序
#include <stdio.h>
/* C语言中,const不是一个真真正正的常量,其代表的含义仅仅是只读。
使用const声明的对象是一个运行时对象,
无法使用其作为某个量的初值、数组的长度、case的值或在类型的情形中使用,
而在C++中可以使用。
*/
#define MaxSize 10 // 定义最大的长度
typedef struct { // 定义一个结构体
int data[MaxSize];
int length;
}SqList;
// 初始化一个顺序表,有些编译器会自动初始化为0,保险起见,我们手动初始化。
void InitList(SqList *L) { // C语言中&表示的不是引用,而是取地址符。C语言引用是“*”
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 ++) // 打印整个data数组
printf("data[%d]=%d\n", i, L.data[i]);
return 0;
}
// C++程序
#include <iostream>
using namespace std;
const int MaxSize = 10; // 定义最大的长度
struct SqList{ // 定义一个结构体
int data[MaxSize];
int length;
}L; // 声明一个顺序表
void InitList(SqList &L) { // 初始化一个顺序表
for (int i = 0; i < MaxSize; i ++)
L.data[i] = 0;
L.length = 0;
}
int main() {
InitList(L); // 初始化顺序表
for (int i = 0; i < MaxSize; i ++) // 打印整个data数组
printf("data[%d]=%d\n", i, L.data[i]);
return 0;
}
2.2.1.1. 局限性
- 顺序便的表长开始确定后就无法更改,因为存储空间是静态的。
2.2.2. 顺序表的动态分配
#define InitSize 10 // 顺序表的初始长度
typedef struct {
ElemType *data; // 动态分配数组的指针
int MaxSize; // 顺序表的最大容量
int length; // 顺序表的当前长度
}SqList; // 顺序表的类型定义(动态分配方式)
2.2.2.1. 动态申请和释放内存空间
C语言
malloc
与free
函数
两个函数的用法请点击malloc和free函数使用注意事项,C语言malloc和free使用详解
// 开辟空间
// malloc函数返回一个指针,需要强行转换为定义的ElemType数据类型的指针
L.data = (ElemType*) malloc(sizeof(ElemType) * InitSize);
// sizeof(ElemType) * InitSize是指要分配InitSize大小的一整片连续内存空间
// 释放空间
free(L.data);
C++
new
与delete
关键字
两个关键字的用法请点击c++中new和delete的使用方法
// 开辟空间
/*
格式1:指针变量名=new 类型标识符;
格式2:指针变量名=new 类型标识符(初始值);
格式3:指针变量名=new 类型标识符[内存单元个数];
*/
L.data = new ElemType[InitSize];
// 释放空间
delete [] L.data; // 不加[]释放的是第一个元素的指针
完整的程序
#include <stdio.h>
#include <stdlib.h> // free, malloc库函数的头文件
#define InitSize 10 // 默认的最大的长度
typedef struct {
int *data; // 动态分配数组的指针
int MaxSize; // 顺序表的最大容量
int length; // 顺序表的当前长度
}SqList; // 顺序表的类型定义(动态分配方式)
void InitList(SqList *L) {
L->data = (int *)malloc(sizeof(int) * InitSize); // 用malloc函数申请一片连续的存储空间
L->MaxSize = InitSize; // 将顺序表的最大容量初始化为默认的最大的长度
L->length = 0; // 将顺序表的当前长度默认为0
}
void IncreaseSize(SqList *L, int len) { // 动态增加数组的长度
int *p = L->data; // 用指针p指向L.data数组
L->data = (int *)malloc(sizeof(int) * (L->MaxSize + len)); // 开辟一片新的连续的存储空间
for (int i = 0; i < L->length; i ++) // 将数据复制到新的区域,这样时间开销很大
L->data[i] = p[i];
L->MaxSize += len; // 更新顺序表可以容纳的最大的长度
free(p); // 释放旧的存储空间
}
// 还可以用realloc函数
// 建议初学者使用 malloc 和 free 更能理解背后过程
void IncreaseSize(SqList *L, int len) {
/*
会先判断当前的指针所指向的内存是否有足够的连续空间,
如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;
如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,
而后释放原来的内存区域
malloc与realloc均不会初始化
*/
L->data = (int *)realloc(L->data, sizeof(int) * 5); // 不写L->data参数和malloc一样
L->MaxSize += len;
}
int main() {
SqList L; // 声明一个顺序表
InitList(&L); // 初始化顺序表
for (int i = 0; i < L.MaxSize; i ++) // 赋值,注意要L.length要++
L.data[i] = i, L.length ++;
IncreaseSize(&L, 5); // 将顺序表扩容5个单位
for (int i = 0; i < L.MaxSize; i ++) // 打印
printf("L[%d]=%d\n", i, L.data[i]);
return 0;
}
malloc
与realloc
的区别请点击malloc与realloc的区别
// C++程序
#include <iostream>
using namespace std;
const int InitSize = 10; // 默认的最大的长度
struct SqList {
int *data;
int MaxSize;
int length;
}L; // 声明一个叫L的结构体
void InitList(SqList &L) {
L.data = new int[InitSize]; // 开辟一个大小为InitSize的连续的存储空间
L.MaxSize = InitSize;
L.length = 0;
}
void IncreaseSize(SqList &L, int len) {
int *p = L.data;
// 开辟一个新的大小为InitSize + len的连续的存储空间
L.data = new int[L.MaxSize + len];
for (int i = 0; i < L.length; i ++)
L.data[i] = p[i];
L.MaxSize += len;
delete [] p; // 释放旧的存储空间
}
int main() {
InitList(L);
for (int i = 0; i < L.MaxSize; i ++)
L.data[i] = i, L.length ++;
IncreaseSize(L, 5);
for (int i = 0; i < L.MaxSize; i ++)
printf("L[%d]=%d\n", i, L.data[i]);
return 0;
}
new
和malloc
的区别请点击new和malloc有何区别?
2.3. 顺序表的特点
- 随机访问,即可以在 O(1) 时间内找到第 i 个元素(data[i - 1],静态与动态都一样)。
- 存储密度高,每个节点只存储数据元素
- 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
- 插入、删除操作不方便,需要移动大量元素
2.4. 顺序表的基本操作
2.4.1. 插入
ListInsert(&L, i, e)
在表L中的第i个位置上插入指定元素e,这里指的是位序,从1开始
2.4.1.1. 静态分配实现
#define MaxSize 10 // 定义最大长度
typedef struct {
ElemType data[MaxSize]; // 用静态数组存放数据
int length; // 顺序表的当前长度
}SqList; // 顺序表的类型定义
完整的程序
// C语言程序
#include <stdio.h>
#include <stdbool.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;
}
// 在顺序表的第i个位置插入元素e
bool ListInsert(SqList *L, int i, int e) {
// 判断i是否有效,i的有效范围[1, length + 1]
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[j - 1]; // 将第i个元素及后的元素往后移动1个单位
L->data[i - 1] = e; // 在i处放上元素e
L->length ++;
return true;
}
int main() {
SqList L;
InitList(&L);
for (int i = 0; i < MaxSize - 1; i ++) // 为前9个赋值数据
L.data[i] = i, L.length ++;
if (ListInsert(&L, 10, 3)) // 在第10个位置插入数据7
puts("Insert success!!!");
else puts("Insert Fatal!!!");
for (int i = 0 ; i < MaxSize; i ++) // 打印
printf("L[%d]=%d\n", i, L.data[i]);
return 0;
}
// C++程序
#include <iostream>
using namespace std;
const int MaxSize = 10;
struct SqList {
int data[MaxSize];
int length;
}L;
// 初始化一个顺序表
void InitList(SqList &L) {
for (int i = 0; i < MaxSize; i ++)
L.data[i] = 0;
L.length = 0;
}
// 在顺序表的第i个位置插入元素e
bool ListInsert(SqList &L, int i, int e) {
// 判断i是否有效,i的有效范围[1, length + 1]
if (i < 1 || i > L.length + 1)
return false;
if (L.length >= MaxSize)
return false;
for (int j = L.length; j >= i; j --) // 将第i个元素及后的元素往后移动1个单位
L.data[j] = L.data[j - 1];
L.data[i - 1] = e; // 在i处放上元素e
return true;
}
int main() {
InitList(L);
for (int i = 0; i < MaxSize - 1; i ++) // 为前9个赋值数据
L.data[i] = i, L.length ++;
if (ListInsert(L, 10, 7)) // 在第10个位置插入数据7
puts("Insert Success!!!");
else puts("Insert Fatal!!!");
for (int i = 0; i < MaxSize; i ++) // 打印
printf("L[%d]=%d\n", i, L.data[i]);
return 0;
}
2.4.1.2. 动态分配实现
并实现了数组越界后的自动扩容
// C语言程序
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define InitSize 10
typedef struct {
int *data; // 动态分配数组的指针
int MaxSize; // 顺序表的最大容量
int length; // 顺序表的当前长度
}SqList; // 顺序表的类型定义(动态分配方式)
void InitList(SqList *L) {
L->data = (int *)malloc(sizeof(int) * InitSize); // 用malloc函数申请一片连续的存储空间
L->MaxSize = InitSize; // 将顺序表的最大容量初始化为默认的最大的长度
L->length = 0; // 将顺序表的当前长度默认为0
}
void IncreaseSize(SqList *L, int len) { // 动态增加数组的长度
int *p = L->data; // 用指针p指向L.data数组
L->data = (int *)malloc(sizeof(int) * (L->MaxSize + len)); // 开辟一片新的连续的存储空间
for (int i = 0; i < L->length; i ++) // 将数据复制到新的区域,这样时间开销很大
L->data[i] = p[i];
L->MaxSize += len; // 更新顺序表可以容纳的最大的长度
free(p); // 释放旧的存储空间
}
bool ListInsert(SqList *L, int i, int e) {
// 判断i是否有效,i的有效范围[1, length + 1]
if (i < 1 || i > L->length + 1)
return false;
if (L->length >= L->MaxSize) // 判断是否越界
IncreaseSize(L, L->length - L->MaxSize + 1); // 越界后动态开辟空间
for (int j = L->length; j >= i; j --) // 将第i个元素及后的元素往后移动1个单位
L->data[j] = L->data[j - 1];
L->data[i - 1] = e; // 在i处放上元素e
L->length ++;
return true;
}
int main() {
SqList L;
InitList(&L);
for (int i = 0; i < L.MaxSize; i ++) // 赋值数据
L.data[i] = i, L.length ++;
if (ListInsert(&L, L.MaxSize + 1, 7)) // 在第MaxSize + 1个位置插入数据7
puts("Insert Success!!!");
else puts("Insert Fatal!!!");
for (int i = 0; i < L.MaxSize; i ++) // 打印
printf("L[%d]=%d\n", i, L.data[i]);
return 0;
}
// C++程序
#include <iostream>
using namespace std;
const int InitSize = 10; // 默认的最大的长度
struct SqList {
int *data;
int MaxSize;
int length;
}L; // 声明一个叫L的结构体
void InitList(SqList &L) {
// 开辟一个大小为InitSize的连续的存储空间
L.data = new int[InitSize];
L.MaxSize = InitSize;
L.length = 0;
}
void IncreaseSize(SqList &L, int len) {
int *p = L.data;
// 开辟一个新的大小为InitSize + len的连续的存储空间
L.data = new int[L.MaxSize + len];
for (int i = 0; i < L.length; i ++)
L.data[i] = p[i];
L.MaxSize += len;
delete [] p; // 释放旧的存储空间
}
bool ListInsert(SqList &L, int i, int e) {
if (i < 1 || i > L.length + 1)
return false;
if (L.length >= L.MaxSize)
IncreaseSize(L, L.length - L.MaxSize + 1);
for (int j = L.length; j >= i; j --)
L.data[j] = L.data[j - 1];
L.data[i - 1] = e;
L.length ++;
return true;
}
int main() {
InitList(L);
for (int i = 0; i < L.MaxSize; i ++) // 赋值数据
L.data[i] = i, L.length ++;
if (ListInsert(L, L.MaxSize + 1, 7)) // 在第MaxSize + 1个位置插入数据7
puts("Insert Success!!!");
else puts("Insert Fatal!!!");
for (int i = 0; i < L.MaxSize; i ++) // 打印
printf("L[%d]=%d\n", i, L.data[i]);
return 0;
}
2.4.1.3. 小结
- 要注意的一点就是在所有线性表的插入操作中,所有的插入都是前插,也就是第 i 个位置的前一个位置进行插入,这是一个小的规定。
2.4.1.3.1. 时间复杂度
- 最好情况:新元素插入到表尾,不需要移动元素 i = n + 1 i=n+1 i=n+1,循环0次;最好时间复杂度 O ( 1 ) O(1) O(1)
- 最坏情况:新元素插入到表头,需要将原有的 n n n个元素全都向后移动 i = 1 i=1 i=1,循环 n n n次,最坏时间复杂度 O ( n ) O(n) O(n)
- 平均情况:假设新元素插入到任何一个位置的概率都相同,概率都是 p = 1 n + 1 p=\frac{1}{n+1} p=n+11, i = 1 i=1 i=1,循环 n n n次, i = 2 i=2 i=2,循环 n − 1 n-1 n−1次… i = n + 1 i=n+1 i=n+1时,循环0次。平均循环次数= n p + ( n − 1 ) p + ( n − 2 ) p + ⋯ 1 ⋅ p = n ( n + 1 ) 2 1 ( n + 1 ) = n 2 np+\left( n-1 \right) p+\left( n-2 \right) p+\cdots 1·p=\frac{n\left( n+1 \right)}{2}\frac{1}{\left( n+1 \right)}=\frac{n}{2} np+(n−1)p+(n−2)p+⋯1⋅p=2n(n+1)(n+1)1=2n,平均时间复杂度 O ( n ) O(n) O(n)
2.4.2. 删除
ListDelete(&L, i, &e)
删除表 L L L中的第 i i i个位置的元素,并用 e e e返回删除元素的值
完整程序
// C语言程序
#include <stdio.h>
#include <stdbool.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;
}
bool ListDelete(SqList *L, int i, int *e) {
// 判断i是否有效,i的有效范围[1, length]
if (i < 1 || i > L->length)
return false;
*e = L->data[i - 1]; // 将被删除的元素赋值给e
for (int j = i; j < L->length; j ++)
L->data[j - 1] = L->data[j];
L->data[L->length - 1] = 0; // 将length位置的元素赋值为0;
L->length --;
return true;
}
int main() {
SqList L;
InitList(&L);
int e = -1;
for (int i = 0; i < MaxSize; i ++)
L.data[i] = i, L.length ++;
if (ListDelete(&L, 5, &e))
printf("Delete %d Success!!!\n", e);
else puts("Delete Fatal!!!");
for (int i = 0; i < MaxSize; i ++)
printf("L[%d]=%d\n", i, L.data[i]);
return 0;
}
// C++程序
#include <iostream>
using namespace std;
const int MaxSize = 10;
struct SqList {
int data[MaxSize];
int length;
}L;
void InitList(SqList &L) {
for (int i = 0; i < MaxSize; i ++)
L.data[i] = 0;
L.length = 0;
}
bool ListDelete(SqList &L, int i, int &e) {
// 判断i是否有效,i的有效范围[1, length]
if (i < 1 || i > L.length)
return false;
e = L.data[i - 1]; // 将被删除的元素赋值给e
for (int j = i; j < L.length; j ++)
L.data[j - 1] = L.data[j];
L.data[L.length - 1] = 0; // 将length位置的元素赋值为0;
L.length --;
return true;
}
int main() {
InitList(L);
int e;
for (int i = 0; i < MaxSize; i ++)
L.data[i] = i, L.length ++;
if (ListDelete(L, 5, e))
printf("Delete %d Success!!!\n", e);
else puts("Delete Fatal!!!");
for (int i = 0; i < MaxSize; i ++)
printf("L[%d]=%d\n", i, L.data[i]);
return 0;
}
2.4.2.1. 小结
- 要注意的一点就是在所有线性表的删除操作中,所有的插入都是前删,也就是第 i 个位置的前一个位置进行删除,这是一个小的规定。
2.4.2.1.1. 时间复杂度
- 最好情况:删除表尾元素,不需要移动其他元素 i = n i = n i=n,循环 0 次;最好时间复杂度 O ( 1 ) O(1) O(1)
- 最坏情况:删除表头元素,需要将后续的 n − 1 n-1 n−1个元素全都向前移动 1 1 1个单位,循环 n − 1 n-1 n−1次;最坏时间复杂度 O ( n ) O(n) O(n);
- 平均情况:假设删除任何一个元素的概率相同,概率都是 p = 1 n p=\frac{1}{n} p=n1, i = 1 i=1 i=1,循环 n n n次, i = 2 i=2 i=2,循环 n − 1 n-1 n−1次… i = n i=n i=n时,循环0次。平均循环次数= ( n − 1 ) p + ( n − 2 ) p + ⋯ 1 ⋅ p = n ( n − 1 ) 2 1 n = n − 1 2 \left( n-1 \right) p+\left( n-2 \right) p+\cdots 1·p=\frac{n\left( n - 1 \right)}{2}\frac{1}{ n}=\frac{n - 1}{2} (n−1)p+(n−2)p+⋯1⋅p=2n(n−1)n1=2n−1,平均时间复杂度 O ( n ) O(n) O(n)
2.4.3. 查找
2.4.3.1. 按位查找
GetElem(L, i)
获取表 L L L中第 i i i个位置的元素的值
#define MaxSize 10
typedef struct {
ElemType data[MaxSize];
int length;
}SqList;
ElemType GetElem(SqList &L, int i) {
return L.data[i - 1];
}
完整的程序
// C语言程序
#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 GetElem(SqList *L, int i) {
return L->data[i - 1]; // 和普通数组访问一样
}
int main() {
SqList L;
InitList(&L);
for (int i = 0; i < MaxSize;i ++)
L.data[i] = i, L.length ++;
printf("%d\n", GetElem(&L, 3));
return 0;
}
// C++程序
#include <iostream>
const int MaxSize = 10;
struct SqList {
int data[MaxSize];
int length;
}L;
void InitList(SqList &L) {
for (int i = 0; i < MaxSize; i ++)
L.data[i] = 0;
L.length = 0;
}
int GetElem(SqList &L, int i) {
return L.data[i - 1]; // 和普通数组访问一样
}
int main() {
InitList(L);
for (int i = 0; i < MaxSize; i ++)
L.data[i] = i, L.length ++;
printf("%d\n", GetElem(L, 5));
return 0;
}
2.4.3.1.1. 时间复杂度
由于顺序表的各个数据元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第 i 个元素——“随机存取”特性。即时间复杂度为 O ( 1 ) O(1) O(1)。
2.4.3.2. 按值查找
LocateElem(L, e)
在表 L L L中查找具有给定关键字值的元素。
#define MaxSize 10
typedef struct {
ElemType data[MaxSize];
int length;
}SqList;
int LocateElem(SqList &L, ElemType e) {
for (int i = 0; i < L.length; i ++)
if (L.data[i] == e)
return i + 1;
return 0;
}
完整的程序
// C语言程序
#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 LocateElem(SqList *L, int e) {
for (int i = 0; i < L->length; i ++)
if (L->data[i] == e)
return i + 1; // 数组下标为i的元素值等于e,返回其位序i+1
return 0; // 查询失败返回0
}
int main() {
SqList L;
InitList(&L);
for (int i = 0; i < MaxSize; i ++)
L.data[i] = i, L.length ++;
printf("%d\n", LocateElem(&L, 5));
return 0;
}
// C++程序
#include <iostream>
using namespace std;
const int MaxSize = 10;
struct SqList {
int data[MaxSize];
int length;
}L;
void InitList(SqList &L) {
for (int i = 0; i < MaxSize; i ++)
L.data[i] = 0;
L.length = 0;
}
int LocateElem(SqList &L, int e) {
for (int i = 0; i < L.length; i ++)
if (L.data[i] == e)
return i + 1; // 数组下标为i的元素值等于e,返回其位序i+1
return 0; // 查询失败返回0
}
int main() {
InitList(L);
for (int i = 0; i < MaxSize; i ++)
L.data[i] = i, L.length ++;
printf("%d\n", LocateElem(L, 5));
return 0;
}
2.4.3.2.1. 时间复杂度
- 最好情况:目标元素在表头循环1次;最好时间复杂度 O ( 1 ) O(1) O(1)
- 最坏情况:目标元素在表尾循环 n n n次;最坏时间复杂度 O ( n ) O(n) O(n)
- 平均情况:假设目标元素出现在任何一个位置的概率相同,都是 1 n \frac{1}{n} n1目标元素在第1位,循环1次;在第2位,循环2次;…… ;在第 n n n位,循环 n n n次,平均循环次数为: 1 ⋅ 1 n + 2 ⋅ 1 n + 3 ⋅ 1 n + ⋯ n ⋅ 1 n = n ( n + 1 ) 2 1 n = n + 1 2 1·\frac{1}{n}+2·\frac{1}{n}+3·\frac{1}{n}+\cdots n·\frac{1}{n}=\frac{n\left( n+1 \right)}{2}\frac{1}{n}=\frac{n+1}{2} 1⋅n1+2⋅n1+3⋅n1+⋯n⋅n1=2n(n+1)n1=2n+1,平均时间复杂度为 O ( n ) O(n) O(n)
2.4.3.3. 小结
- 《数据结构》考研初试中,手写代码可以直接用“==”,无论 ElemType 是基本数据类型还是
结构类型 - 手写代码主要考察学生是否能理解算法思想,不会严格要求代码完全可运行
- 有的学校考《C语言程序设计》,那么…也许就要语法严格一些
3. 链表
3.1. 链表的基本概念
定义:链式存储︰用一组任意的存储单元存储线性表中的数据元素。用这种方法存储的线性表简称线性链表。每一个结点只包含一个指针域的链表称为单链表
结点的组成
-
数据域:存放数据元素
-
指针域:存放后继结点的地址
首元结点:链表中存储第一个数据元素 a 1 a_1 a1的结点
头指针:指向链表中第一个结点的指针,通常用用来标识一个单链表
头结点:在链表的首元结点之前附加的一个结点,指针域指向线性表的首元结点,头结点的数据域可以不设任何信息,也可以只放空表标志和表长等信息,但此结点不能计入链表长度值
头指针与头结点的区分:不管是否带有头结点,头指针均指向链表的首元结点,而头结点是带头结点的链表中的首元结点,结点内通常不存储信息
不带头结点VS带头结点
- 由于首元结点的位置被存储在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无需特殊处理
- 无论链表是否为空,头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就统一了
单链表:结点只有一个指针域的链表,称为单链表或线性链表
双链表:有两个指针域的链表,称为双链表
循环链表:首尾相接的链表称为循环链表
3.2. 链表的优缺点
3.2.1. 优点
- 数据元素的个数可以自由扩充
- 插入、删除等操作不必移动数据,只需修改链接指针,修改效率较高
3.2.2. 缺点
- 存储密度小
- 存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问
3.3. 单链表
3.3.1. 单链表的实现
typedef struct { // 定义单链表结点类型
ElemType val; // 每个结点存放一个数据元素,数据域
struct LNode *next; // 指针指向下一个结点,指针域
}LNode, *LinkList;
// LNode *强调的是一个结点,LinkList强调的是一个链表
3.3.2. 单链表的基本操作
3.3.2.1. 头插法建立单链表
先入后出,先插入的在后面
3.3.2.1.1. 不带头结点
核心代码
// C语言程序
// 头插法,逆向建立单链表
ListNode *ListHeadInsert(LinkList head) {
// 头插法读入与输出相反
for (int i = 1; i < 10; i ++) { // 生成一个链表
ListNode *p = (ListNode *)malloc(sizeof(ListNode));
/*
头插法与尾插法的不同之处主要在此
新结点p的指针域指向头指针指向的区域
*/
p->next = head;
p->val = i;
// 令头指针指向新结点
head = p;
}
return head;
}
// C++代码
// 头插法,逆向建立单链表
ListNode *ListHeadInsert(ListNode *head) {
// 头插法读入与输出相反
for (int i = 1; i < 10; i ++) { // 生成一个链表
ListNode *p = new ListNode(i);
/*
头插法与尾插法的不同之处主要在此
新结点p的指针域指向头指针指向的区域
*/
p->next = head;
// 另头指针指向新结点
head = p;
}
return head;
}
3.3.2.1.2. 带头结点
核心代码
// C语言代码
// 头插法,逆向建立单链表
ListNode *ListHeadInsert(LinkList head) {
// 头插法读入与输出相反
for (int i = 1; i < 10; i ++) { // 生成一个链表
ListNode *p = (ListNode *)malloc(sizeof(ListNode));
/*
头插法与尾插法的不同之处主要在此
新结点p的指针域指向头指针指向的区域
*/
p->next = head->next;
p->val = i;
// 令头指针指向新结点
head->next = p;
}
return head;
}
// C++代码
// 头插法,逆向建立单链表
ListNode *ListHeadInsert(ListNode *head) {
// 头插法读入与输出相反
for (int i = 1; i < 10; i ++) { // 生成一个链表
ListNode *p = new ListNode(i);
/*
头插法与尾插法的不同之处主要在此
新结点p的指针域指向头指针指向的区域
*/
p->next = head->next;
// 另头指针指向新结点
head->next = p;
}
return head;
}
3.3.2.2. 尾插法建立单链表
3.3.2.2.1. 不带头结点
核心代码
// C语言程序
// 尾插法,正向建立单链表
ListNode *ListTailInsert(LinkList head) {
ListNode *r = head;
for (int i = 1; i < 10; i ++) { // 生成一个链表
ListNode *p = (ListNode *)malloc(sizeof(ListNode));
/*
头插法与尾插法的不同之处主要在此
*/
p->val = i;
if (!head) { // 特判下head初始化后指向NULL
head = p;
r = p;
continue;
}
// 令尾指针的next指针指向新结点
r->next = p;
r = p; // 尾指针指向新结点
}
r->next = NULL; // 将尾指针置空
return head;
}
// C++代码
// 尾插法,正向建立单链表
ListNode *ListHeadInsert(ListNode *head) {
ListNode *r = head;
for (int i = 1; i < 10; i ++) { // 生成一个链表
ListNode *p = new ListNode(i);
/*
头插法与尾插法的不同之处主要在此
*/
if (!head) {
head = p;
r = p;
continue;
}
r->next = p;
r = p;
}
r->next = NULL;
return head;
}
3.3.2.2.2. 带头结点
核心代码
// C语言代码
// 尾插法,正向建立单链表
ListNode *ListHeadInsert(LinkList head) {
LinkList r = head;
for (int i = 1; i < 10; i ++) { // 生成一个链表
ListNode *p = (ListNode *)malloc(sizeof(ListNode));
/*
头插法与尾插法的不同之处主要在此
*/
p->val = i;
r->next = p;
r = p;
}
r->next = NULL;
return head;
}
// C++代码
// 尾插法,正向建立单链表
ListNode *ListTailInsert(ListNode *head) {
ListNode *r = head;
for (int i = 1; i < 10; i ++) { // 生成一个链表
ListNode *p = new ListNode(i);
/*
头插法与尾插法的不同之处主要在此
*/
r->next = p;
r = p;
}
r->next = NULL;
return head;
}
3.3.2.3. 单链表的查找
3.3.2.3.1. 按序号查找结点值
GetElem(L, i)
:按位查找,获取表
L
L
L中第
i
i
i个位置的元素的值
3.3.2.3.1.1. 带头结点
核心代码
// C语言程序
ListNode *GetElem(LinkList head, int i) {
if (i < 0)
return NULL;
ListNode *p; // 指针p指向当前扫描到的结点
int j = 0; // 当前指针p指向的第几个结点
p = head; // 头结点是第0个结点不存储数据
while (p && j < i) // 循环找到第i个结点
p = p->next, j ++;
return p;
}
// C++程序
ListNode *GetElem(ListNode *head, int i) {
if (i < 0)
return NULL;
ListNode *p; // 指针p指向当前扫描到的结点
int j = 0; // 当前指针p指向的第几个结点
p = head; // 头结点是第0个结点不存储数据
while (p && j < i) // 循环找到第i个结点
p = p->next, j ++;
return p;
}
3.3.2.3.2. 按值查找表结点
3.3.2.3.2.1. 带头结点
核心代码
// C语言程序
ListNode *LocateElem(LinkList head, int e) {
ListNode *p = head->next;
while (p && p->data != e)
p = p->next;
return p;
}
// C++程序
ListNode *LocateElem(ListNode *head, int e) {
ListNode *p = head->next;
while (p && p->val != e)
p = p->next;
return p;
}
3.3.2.4. 按结点插入
3.3.2.4.1. 按指定结点的前插操作
InsertPriorNode(p e)
:在指定结点
p
p
p之前插入元素
e
e
e
核心代码
// C语言程序
bool InsertPriorNode(ListNode *p, int e) {
if (!p)
return false;
ListNode *q = (ListNode *)malloc(sizeof(ListNode));
if (!q)
return false;
q->next = p->next;
p->next = q;
q->val = p->val; // 将指定结点p中的val赋值给新结点的val
p->val = e; // 将新结点的val赋值e
return true;
}
// C++程序
bool InsertPriorNode(ListNode *p, int e) {
if (!p)
return false;
ListNode *q = new ListNode(e);
if (!q)
return false;
q->next = p->next;
p->next = q;
q->val = p->val; // 将指定结点p中的val赋值给新结点的val
p->val = e; // 将新结点的val赋值e
return true;
}
3.3.2.4.2. 按指定结点的后插操作
InsertNextNode(p, e)
:在结点
p
p
p后面插入指定元素
e
e
e
核心代码
// C语言程序
bool InsertNextNode(ListNode *p, int e) {
if (!p)
return false;
ListNode *q = (ListNode *)malloc(sizeof(ListNode));
if (!q)
return false;
q->val = e; // 用结点q保存数据e
q->next = p->next;
p->next = q;
return true;
}
// C++程序
bool InsertNextNode(ListNode *p, int e) {
if (!p)
return false;
ListNode *q = new ListNode(e);
if (!q)
return false;
q->next = p->next;
p->next = q;
return true;
}
3.3.2.5. 按位序插入
ListInsert(&L, i, e)
:在表
L
L
L的第
i
i
i个位置插入指定元素e
3.3.2.5.1. 不带头结点
不存在 “第0个”结点,因此 i=1 时需要特殊处理
核心代码
// C语言程序
// 按位序插入(不带头结点)
bool ListInsert(LinkList *head, int i, int e) {
if (i < 1)
return false;
if (i == 1) { // 插入第1个结点的操作与其他结点不同,需要特殊处理
ListNode *p = (ListNode *)malloc(sizeof(ListNode));
p->val = e;
p->next = *head;
*head = p; // 头指针指向新结点
return true;
}
ListNode *p; // 指针指向当前扫描到的结点
int j = 1; // 当前p指针指向的第几个结点
p = *head; // p指向第1个结点(首元结点,不是头结点)
while (p && j < i - 1)
p = p->next, j ++;
InsertNextNode(p, e);
}
// C++代码
// 按位序插入(不带头结点)
bool ListInsert(ListNode *&head, int i, int e) {
if (i < 1)
return false;
if (i == 1) {
ListNode *p = new ListNode(e);
p->next = head;
head = p;
return true;
}
ListNode *p; // 指针指向扫描到的结点
int j = 1; // 当前指针p指向的是第几个结点
p = head; // p指向第一个结点(首元结点)不是头结点
while (p && j < i - 1)
p = p->next, j ++;
InsertNextNode(p, e);
}
3.3.2.5.2. 带头结点
头结点可以看作是“第0个”结点
核心代码
// C语言代码
// 按位序插入(带头结点)
bool ListInsert(LinkList head, int i, int e) {
if (i < 1)
return false;
ListNode *p = GetElem(head, i - 1);
InsertNextNode(p, e);
}
// C++代码
// 按位序插入(带头结点)
bool ListInsert(ListNode *head, int i, int e) {
if (i < 1)
return false;
ListNode *p = GetElem(head, i - 1);
InsertNextNode(p, e);
}
3.3.2.6. 单链表的删除
3.3.2.6.1. 按位序删除
ListDelete(&L, i, &e)
:删除操作,删除表
L
L
L中第
i
i
i个位置的元素,并用
e
e
e返回删除元素的值
3.3.2.6.1.1. 带头结点
核心代码
// C语言程序
bool ListDelete(LinkList head, int i, int *e) {
if (i < 1)
return false;
ListNode *p = GetElem(head, i - 1); // 找到第i个结点
if (!p || !p->next)
return false;
ListNode *q = p->next; // 令指针q指向被删除的结点
e = q->val; // 用e返回被删除的值
p->next = q->next; // 将被删除的结点从链中断开
free(q) // 释放被删除的结点
return true;
}
// C++程序
bool ListDelete(ListNode *head, int i, int &e) {
if (i < 1)
return false;
ListNode *p = GetElem(head, i - 1); // 找到第i个结点
if (!p || !p->next)
return false;
ListNode *q = p->next; // 令指针q指向被删除的结点
p->next = q->next; // 将被删除的结点从链中断开
delete q; // 释放被删除的结点
return true;
}
3.3.2.6.2. 按指定结点的删除
DeleteNode(&p)
:删除指定结点p
方法一:传入链表头指针,循环遍历找到该结点的前一个结点
方法二:偷天换日,交换val,但是当要删除最后一个结点的时候只能使用方法一
核心代码
// C语言程序
bool DeleteNode(ListNode *p) {
if (!p)
return false;
ListNode *q = p->next; // 令q指针指向p的后继结点
p->val = q->val; // 和后继结点交换数据
p->next = q->next; // 将结点q从链表中断开
free(q); // 释放结点q
return true;
}
// C++程序
bool DeleteNode(ListNode *p) {
if (!p)
return false;
ListNode *q = p->next;
p->val = q->val;
p->next = q->next;
delete q;
return true;
}
3.3.2.7. 求表长
3.3.2.7.1. 带头结点
核心代码
// C语言程序
int Length(LinkList head) {
int len = 0;
ListNode *p = head;
while (p->next)
p = p->next, len ++;
return len;
}
// C++程序
int Length(ListNode *head) {
int len = 0;
ListNode *p = head;
while (p->next)
p = p->next, len ++;
return len;
}
3.3.3. 单链表的完整程序
单链表的完整程序👉单链表基本操作的完整程序
3.4. 双链表
只介绍初始化、插入、删除、销毁。按值查找,和按位查找均与单链表相同
typedef struct DListNode {
ElemType val;
struct DListNode *prior, *next;
}DListNode, *DLinkList;
3.4.1. 带头结点
3.4.1.1. 初始化
核心代码
// C语言程序
bool DInitList(DLinkList *head) {
DListNode *p = (DListNode *)malloc(sizeof(DListNode));
if (!p)
return false;
p->prior = NULL; // 头结点的prior永远指向NULL
p->next = NULL;
*head = p;
return true;
}
// C++程序
bool DInitList(DListNode *&head) {
DListNode *p = new DListNode();
if (!p)
return false;
p->prior = NULL; // 头结点的prior永远指向NULL
p->next = NULL;
head = p;
return true;
}
3.4.1.2. 插入
核心代码
// C语言程序和C++程序相同
// 在p结点之后插入s结点
bool InsertDListNode(DListNode *p, DListNode *s) {
if (!p || !s)
return false;
s->next = p->next;
if (!p->next)
p->next->prior = s;
s->prior = p;
p->next = s;
return true;
}
3.4.1.3. 删除
核心代码
// C语言程序和C++程序相同
// 删除p的后继结点
bool DeleteNextDListNode(DListNode *p) {
if (!p || !p->next)
return false;
DListNode *q = p->next;
p->next = q->next;
if (!q->next)
q->next->prior = p;
free(q);
// delete q // C++可以写这个
return true;
}
3.4.1.4. 销毁
核心代码
// C语言程序
// 销毁双链表
void DestoryList(DLinkList *head) {
// 循环释放每个结点
while ((*head)->next)
DeleteNextDListNode(*head);
free(*head); // 释放头结点
*head = NULL; // 头指针指向NULL
}
// C++程序
// 销毁双链表
void DestoryList(DListNode *&head) {
// 循环释放每个结点
while (head->next)
DeleteNextDListNode(head);
delete head; // 释放头结点
head = NULL; // 头指针指向NULL
}
3.5. 循环链表
3.5.1. 循环单链表
核心代码
// C语言程序
// 初始化一个循环单链表
bool InitList(LinkList *head) {
ListNode *p = (ListNode *)malloc(sizeof(ListNode));
if (!p)
return false;
head = p;
head->next = head; // 头结点next的指针指向头结点
return true;
}
// C++程序
bool InitList(ListNode *&head) {
ListNode *p = new ListNode();
if (!p)
return false;
head = p;
head = p;
head->next = head;
return false;
}
3.5.2. 循环双链表
初始化
核心代码
// C语言程序
bool InitDLinkList(DLinkList *head) {
ListNode *p = (ListNode *)malloc(sizeof(ListNode));
if (!p)
return false;
head = p;
head->prior = head;
head->next = head;
}
// C++程序
bool InitDLinkList(DListNode *&head) {
ListNode *p = new ListNode();
if (!p)
return false;
head = p;
head->prior = head;
head->next = head;
return true;
}
插入
核心代码
// C语言程序
bool InsertNextDListNode(DListNode *p, DListNode *s) {
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;
}
// C++程序
3.6. 顺序表VS链表
3.6.1. 存储读写方式
顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素。例如在第 i i i个位置上执行存或取的操作,顺序表仅需一次访问,而链表则需从表头开始依次访问 i i i次。
3.6.2. 逻辑结构与物理结构
采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。而采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示的。
3.6.3. 查找、插入和删除操作
对于按值查找,顺序表无序时,两者的时间复杂度均为 O ( n ) O(n) O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为 O ( l o g n ) O(logn) O(logn)
对于按序号查找,顺序表支持随机访问,时间复杂度仅为 O ( 1 ) O(1) O(1),而链表的平均时间复杂度为 O ( n ) O(n) O(n)。顺序表的插入、删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需修改相关结点的指针域即可。由于链表的每个结点都带有指针域,故而存储密度不够大。
3.6.4. 空间分配
顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。动态存储分配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效
如何选取存储结构
-
基于存储方面:长度或规模难以估计,不宜选择顺序表,链表不用事先估计存储规模,但是链表的存储密度比较低,显然链式存储结构的存储密度是小于1的
-
基于运算的考虑:顺序表是开辟一大片连续空间存储,可以随机存取,按序号访问的时间复杂度为 O ( 1 ) O(1) O(1)。而链表在物理逻辑上不相邻,所以访问元素的时间复杂度为 O ( n ) O(n) O(n),因此,经常做的运算是按序号访问数据元素,显然顺序表优于链表。在顺序表中插入删除操作的时候,平均移动一半的元素,当数据元素量较大的时候,速度会变得很慢,在链表中插入,删除的时候,虽然也要找插入的位置,但是主要的是比较操作,显然链表优于顺序表
-
顺序表容易实现,任何高级语言都有数组类型,而链表的操作是基于指针,显然顺序表实现较链表简单
注:如果您通过阅读本文解决了问题,恳请您留下一个宝贵的赞再离开,如果不嫌弃的话,点个关注再走吧,非常欢迎您能够在讨论区指出此文的不足处,博主会及时对文章加以修正 !如果有任何问题,欢迎评论,非常乐意为您解答!( •̀ ω •́ )✧