目录
单链表内容回顾
已经到大三下学期,有必要对基本数据结构和一些基本算法进行回顾和复习,每周更新,包括基本数据结构以及一些基本算法,这里使用C++语言实现较多,在时间有空余的情况下会更新 Java 实现基本的数据结构和算法。
这里每实现一个函数都会进行一个测试,测试的代码包括已经实现的全部功能,因此文章可能会比较繁琐,文章仅供参考,如有错误,欢迎批评指教。
文章 PDF 链接:
「单链表内容回顾.pdf」https://www.aliyundrive.com/s/vn3XUQwMG7p
点击链接保存,或者复制本段内容,打开「阿里云盘」APP ,无需下载极速在线查看,视频原画倍速播放。
单链表的定义和表示
线性表链式存储结构的特点是:用一组任意的存储单元存储线表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素 a i a_i ai 与其直接后继数据元素 a i + 1 a_i+1 ai+1 之间的逻辑关系,对数据元素 a i a_i ai 来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。这两部分信息组成数据元素 a i a_i ai 的存储映像,称为结点(node)。
单链表:由于链表的每个结点中只包含一个指针域,故称为线性链表或单链表
单链表特点:
- 包括两个域:数据域(data),指针域(node)
- 指针域中存储的信息称为指针或链,存储直接后继
- n个结点链结成一个链表
结点的实现
关于结点的实现代码如下:
typedef struct LNode
{
// 这里数据域使用 int 型, 这里使用了递归定义
int data;
LNode *next;
}LNode, *LinkList;
说明:
- 这里对同一结构体指针类型起了两个名称,LinkList 与 * LNode,两者本质等价。习惯上用 LinkList 定义单链表,强调定义的是某个单链表的头指针;用 LNode * 定义指向单链表中任意结点的指针变量。
- 区分指针变量和结点变量两个不同的概念,若定义 LinkList p 或 LNode *p,则 p 为指向某结点的指针变量,表示结点的地址;而 * p 为对应的结点变量,表示该节点的名称。
头插法创建单链表
头插法又称为前插法,是通过将新结点逐个插入链表的头部(头结点)之后来创建链表,每次申请一个新节点,读入相应的数据元素值,然后将新节点插入到头结点之后。
使用头插法创建单链表时,注意是逆序的。
代码实现:
/**
* 使用头插法创建单链表,注意是逆置创建
*
* @param &L 指向头结点的头指针
* @param n 表示插入结点的个数
*
* */
void createHead(LinkList &L, int n)
{
// 首先将头结点的指针域置空
L = new LNode;
L->next = NULL;
for(int i = 0; i < n; i++)
{
// 生成新节点
LNode *p = new LNode;
// 输入新节点中的 data
cin >> p->data;
// 实现图解 ①
p->next = L->next;
// 实现图解 ②
L->next = p;
}
}
这里写一个遍历链表的函数进行测试:
/**
* 遍历单链表
*
* @param &L 表示指向头结点的头指针
*
* */
void getElementAll(LinkList &L)
{
LNode *p = L->next;
while(p != NULL)
{
cout << p->data << " ";
p = p->next;
}
}
测试头插法创建单链表
这里进行测试,以上完整代码如下:
#include <iostream>
using namespace std;
typedef struct LNode
{
// 这里数据域使用 int 型, 这里使用了递归定义
int data;
LNode *next;
}LNode, *LinkList;
/**
* 使用头插法创建单链表,注意是逆置创建
*
* @param &L 指向头结点的头指针
* @param n 表示插入结点的个数
*
* */
void createHead(LinkList &L, int n)
{
// 首先将头结点的指针域置空
L = new LNode;
L->next = NULL;
for(int i = 0; i < n; i++)
{
// 生成新节点
LNode *p = new LNode;
// 输入新节点中的 data
cin >> p->data;
// 实现图解 ①
p->next = L->next;
// 实现图解 ②
L->next = p;
}
}
/**
* 遍历单链表
*
* @param &L 表示指向头结点的头指针
*
* */
void getElementAll(LinkList &L)
{
LNode *p = L->next;
while(p != NULL)
{
cout << p->data << " ";
p = p->next;
}
}
int main()
{
LinkList L;
cout << "请输入单链表的长度:";
// n 表示单链表的长度
int n;
cin >> n;
cout << "请输入链表中元素: ";
// 测试头插法创建单链表
createHead(L, n);
// 遍历链表
getElementAll(L);
return 0;
}
测试结果如图:
尾插法创建单链表
尾插法又称为后插法,是通过将新结点逐个插入到链表的尾部来创建链表。通头插法一样,每次申请一个新节点,读入相应的数据元素值。不同的是,为了使新结点能够插入到表尾,需要增加一个尾指针 r 指向链表的尾结点。
尾插法创建单链表是按正序的
代码实现:
/**
* 尾插法创建单链表
*
* @param &L 表示指向头结点的头指针
* @param n 表示要插入结点个数
*
* */
void createTail(LinkList &L, int n)
{
L = new LNode;
L->next = NULL;
// 首先让指针 r 指向最后一个结点
LNode *r = L;
for(int i = 0; i < n; i++)
{
LNode *p = new LNode;
// 实现第 ① 步
p->next = NULL;
cin >> p->data;
// 实现第 ② 步
r->next = p;
// 实现第三步
r = p;
}
}
测试尾插法创建单链表
这里进行测试,以上完整代码如下:
#include <iostream>
using namespace std;
typedef struct LNode
{
// 这里数据域使用 int 型, 这里使用了递归定义
int data;
LNode *next;
}LNode, *LinkList;
/**
* 使用头插法创建单链表,注意是逆置创建
*
* @param &L 指向头结点的头指针
* @param n 表示插入结点的个数
*
* */
void createHead(LinkList &L, int n)
{
// 首先将头结点的指针域置空
L = new LNode;
L->next = NULL;
for(int i = 0; i < n; i++)
{
// 生成新节点
LNode *p = new LNode;
// 输入新节点中的 data
cin >> p->data;
// 实现图解 ①
p->next = L->next;
// 实现图解 ②
L->next = p;
}
}
/**
* 尾插法创建单链表
*
* @param &L 表示指向头结点的头指针
* @param n 表示要插入结点个数
*
* */
void createTail(LinkList &L, int n)
{
L = new LNode;
L->next = NULL;
// 首先让指针 r 指向最后一个结点
LNode *r = L;
for(int i = 0; i < n; i++)
{
LNode *p = new LNode;
// 实现第 ① 步
p->next = NULL;
cin >> p->data;
// 实现第 ② 步
r->next = p;
// 实现第三步
r = p;
}
}
/**
* 遍历单链表
*
* @param &L 表示指向头结点的头指针
*
* */
void getElementAll(LinkList &L)
{
LNode *p = L->next;
while(p != NULL)
{
cout << p->data << " ";
p = p->next;
}
}
int main()
{
LinkList L;
cout << "请输入单链表的长度:";
// n 表示单链表的长度
int n;
cin >> n;
cout << "请输入链表中元素: ";
// 测试头插法创建单链表
// createHead(L, n);
// 测试尾插法创建单链表
createTail(L, n);
// 遍历链表
getElementAll(L);
return 0;
}
测试结果如图所示:
单链表的常见操作
取值(根据序号取值)
和顺序表不同,链表中逻辑相邻的结点并没有存储在物理相邻的单元中,这样,根据给定的结点位置序号 i i i ,在链表中获取该结点的值不能像顺序表那样随机访问,而只能从链表的首元结点出发,顺着链域 next 逐个结点向下访问。
代码实现:
/**
* 取值(根据序号取值)
*
* @param &L 表示指向头结点的头指针
* @param i 表示输入的序号
* @param &e 通过序号 i 查找到的值,传递回去
*
* */
void getElement(LinkList &L, int i, int &e)
{
LNode *p = L;
int j = 0;
// 这里 j <= i 的时候表示和数组下标相同,都是从 0 开始
// 如果写成 j < i,表示从 1 开始
// 为了符合数组从 0 开始的习惯,这里写成 j <= i
while((p != NULL) && j <= i)
{
p = p->next;
j++;
}
e = p->data;
}
测试根据下标取值:
完整代码如下:
#include <iostream>
using namespace std;
typedef struct LNode
{
// 这里数据域使用 int 型, 这里使用了递归定义
int data;
LNode *next;
}LNode, *LinkList;
/**
* 使用头插法创建单链表,注意是逆置创建
*
* @param &L 指向头结点的头指针
* @param n 表示插入结点的个数
*
* */
void createHead(LinkList &L, int n)
{
// 首先将头结点的指针域置空
L = new LNode;
L->next = NULL;
for(int i = 0; i < n; i++)
{
// 生成新节点
LNode *p = new LNode;
// 输入新节点中的 data
cin >> p->data;
// 实现图解 ①
p->next = L->next;
// 实现图解 ②
L->next = p;
}
}
/**
* 尾插法创建单链表
*
* @param &L 表示指向头结点的头指针
* @param n 表示要插入结点个数
*
* */
void createTail(LinkList &L, int n)
{
L = new LNode;
L->next = NULL;
// 首先让指针 r 指向最后一个结点
LNode *r = L;
for(int i = 0; i < n; i++)
{
LNode *p = new LNode;
// 实现第 ① 步
p->next = NULL;
cin >> p->data;
// 实现第 ② 步
r->next = p;
// 实现第三步
r = p;
}
}
/**
* 遍历单链表
*
* @param &L 表示指向头结点的头指针
*
* */
void getElementAll(LinkList &L)
{
LNode *p = L->next;
while(p != NULL)
{
cout << p->data << " ";
p = p->next;
}
}
/**
* 取值(根据序号取值)
*
* @param &L 表示指向头结点的头指针
* @param i 表示输入的序号
* @param &e 通过序号 i 查找到的值,传递回去
*
* */
void getElement(LinkList &L, int i, int &e)
{
LNode *p = L;
int j = 0;
// 这里 j <= i 的时候表示和数组下标相同,都是从 0 开始
// 如果写成 j < i,表示从 1 开始
// 为了符合数组从 0 开始的习惯,这里写成 j <= i
while((p != NULL) && j <= i)
{
p = p->next;
j++;
}
e = p->data;
}
int main()
{
LinkList L;
cout << "请输入单链表的长度:";
// n 表示单链表的长度
int n;
cin >> n;
cout << "请输入链表中元素: ";
// 测试头插法创建单链表
// createHead(L, n);
// 测试尾插法创建单链表
createTail(L, n);
// 遍历链表
// getElementAll(L);
// 测试根据下标获取元素值
cout << "请输入下标:";
int i, e;
cin >> i;
getElement(L, i, e);
cout << "下标" << i << "的值为:" << e << endl;
return 0;
}
测试结果:
查找(根据给定值查找)
链表中按值查找的过程和顺序表类似,从链表的首元结点出发,依次将结点值和给定值 e 进行比较,返回查询结果。
代码实现:
/**
* 根据给定值查找结点
*
* @param L 表示指向头结点的头指针
* @param e 表示给定值
* @return 返回值是结点的地址,如果不存在返回 NULL
*
* */
LNode *LocationElement(LinkList L, int e)
{
// 首先 p 指向首元结点
LNode *p = L->next;
while(p)
{
if(p->data == e)
{
// 查找成功,返回地址
return p;
}
p = p->next;
}
// 查找失败,返回NULL,此时 p == NULL;
return p;
}
测试根据给定值查找:
完整代码如下:
#include <iostream>
using namespace std;
typedef struct LNode
{
// 这里数据域使用 int 型, 这里使用了递归定义
int data;
LNode *next;
}LNode, *LinkList;
/**
* 使用头插法创建单链表,注意是逆置创建
*
* @param &L 指向头结点的头指针
* @param n 表示插入结点的个数
*
* */
void createHead(LinkList &L, int n)
{
// 首先将头结点的指针域置空
L = new LNode;
L->next = NULL;
for(int i = 0; i < n; i++)
{
// 生成新节点
LNode *p = new LNode;
// 输入新节点中的 data
cin >> p->data;
// 实现图解 ①
p->next = L->next;
// 实现图解 ②
L->next = p;
}
}
/**
* 尾插法创建单链表
*
* @param &L 表示指向头结点的头指针
* @param n 表示要插入结点个数
*
* */
void createTail(LinkList &L, int n)
{
L = new LNode;
L->next = NULL;
// 首先让指针 r 指向最后一个结点
LNode *r = L;
for(int i = 0; i < n; i++)
{
LNode *p = new LNode;
// 实现第 ① 步
p->next = NULL;
cin >> p->data;
// 实现第 ② 步
r->next = p;
// 实现第三步
r = p;
}
}
/**
* 遍历单链表
*
* @param &L 表示指向头结点的头指针
*
* */
void getElementAll(LinkList &L)
{
LNode *p = L->next;
while(p != NULL)
{
cout << p->data << " ";
p = p->next;
}
}
/**
* 取值(根据序号取值)
*
* @param &L 表示指向头结点的头指针
* @param i 表示输入的序号
* @param &e 通过序号 i 查找到的值,传递回去
*
* */
void getElement(LinkList &L, int i, int &e)
{
LNode *p = L;
int j = 0;
// 这里 j <= i 的时候表示和数组下标相同,都是从 0 开始
// 如果写成 j < i,表示从 1 开始
// 为了符合数组从 0 开始的习惯,这里写成 j <= i
while((p != NULL) && j <= i)
{
p = p->next;
j++;
}
e = p->data;
}
/**
* 根据给定值查找结点
*
* @param L 表示指向头结点的头指针
* @param e 表示给定值
* @return 返回值是结点的地址,如果不存在返回 NULL
*
* */
LNode *LocationElement(LinkList L, int e)
{
// 首先 p 指向首元结点
LNode *p = L->next;
while(p)
{
if(p->data == e)
{
// 查找成功,返回地址
return p;
}
p = p->next;
}
// 查找失败,返回NULL,此时 p->next == NULL;
return p;
}
int main()
{
LinkList L;
cout << "请输入单链表的长度:";
// n 表示单链表的长度
int n;
cin >> n;
cout << "请输入链表中元素: ";
// 测试头插法创建单链表
// createHead(L, n);
// 测试尾插法创建单链表
createTail(L, n);
// 遍历链表
// getElementAll(L);
// 测试根据下标获取元素值
// cout << "请输入下标:";
int i, e;
// cin >> i;
// getElement(L, i, e);
// cout << "下标" << i << "的值为:" << e << endl;
// 测试给定值获取结点
cout << "请输入值:";
cin >> e;
LNode *p = LocationElement(L, e);
if(p)
{
cout << "此结点值为: " << p->data;
}
else
{
cout << "此结点不存在!";
}
return 0;
}
测试结果:
插入
要在单链表的两个数据元素之间插入一个元素
代码实现:
/**
* 根据下标,在该下标前插入新结点
*
* @param &L 表示指向头结点的头指针
* @param i 表示下标,这里下标从 0 开始,和数组一样,便于理解
* @param e 表示要插入的新结点的 data
*
* */
void insertElement(LinkList &L, int i, int e)
{
// 首先 r 指向头结点
LNode *r = L;
// j 用于定位指向 i 的前一个结点位置
int j = 0;
while(j < i)
{
r = r->next;
j++;
}
// 创建新节点
LNode *p = new LNode;
p->data = e;
// 实现 ①
p->next = r->next;
// 实现 ②
r->next = p;
}
测试插入
完整代码:
#include <iostream>
using namespace std;
typedef struct LNode
{
// 这里数据域使用 int 型, 这里使用了递归定义
int data;
LNode *next;
}LNode, *LinkList;
/**
* 使用头插法创建单链表,注意是逆置创建
*
* @param &L 指向头结点的头指针
* @param n 表示插入结点的个数
*
* */
void createHead(LinkList &L, int n)
{
// 首先将头结点的指针域置空
L = new LNode;
L->next = NULL;
for(int i = 0; i < n; i++)
{
// 生成新节点
LNode *p = new LNode;
// 输入新节点中的 data
cin >> p->data;
// 实现图解 ①
p->next = L->next;
// 实现图解 ②
L->next = p;
}
}
/**
* 尾插法创建单链表
*
* @param &L 表示指向头结点的头指针
* @param n 表示要插入结点个数
*
* */
void createTail(LinkList &L, int n)
{
L = new LNode;
L->next = NULL;
// 首先让指针 r 指向最后一个结点
LNode *r = L;
for(int i = 0; i < n; i++)
{
LNode *p = new LNode;
// 实现第 ① 步
p->next = NULL;
cin >> p->data;
// 实现第 ② 步
r->next = p;
// 实现第三步
r = p;
}
}
/**
* 遍历单链表
*
* @param &L 表示指向头结点的头指针
*
* */
void getElementAll(LinkList &L)
{
LNode *p = L->next;
while(p != NULL)
{
cout << p->data << " ";
p = p->next;
}
}
/**
* 取值(根据序号取值)
*
* @param &L 表示指向头结点的头指针
* @param i 表示输入的序号
* @param &e 通过序号 i 查找到的值,传递回去
*
* */
void getElement(LinkList &L, int i, int &e)
{
LNode *p = L;
int j = 0;
// 这里 j <= i 的时候表示和数组下标相同,都是从 0 开始
// 如果写成 j < i,表示从 1 开始
// 为了符合数组从 0 开始的习惯,这里写成 j <= i
while((p != NULL) && j <= i)
{
p = p->next;
j++;
}
e = p->data;
}
/**
* 根据给定值查找结点
*
* @param L 表示指向头结点的头指针
* @param e 表示给定值
* @return 返回值是结点的地址,如果不存在返回 NULL
*
* */
LNode *LocationElement(LinkList L, int e)
{
// 首先 p 指向首元结点
LNode *p = L->next;
while(p)
{
if(p->data == e)
{
// 查找成功,返回地址
return p;
}
p = p->next;
}
// 查找失败,返回NULL,此时 p->next == NULL;
return p;
}
/**
* 根据下标,在该下标前插入新结点
*
* @param &L 表示指向头结点的头指针
* @param i 表示下标,这里下标从 0 开始,和数组一样,便于理解
* @param e 表示要插入的新结点的 data
*
* */
void insertElement(LinkList &L, int i, int e)
{
// 首先 r 指向头结点
LNode *r = L;
// j 用于定位指向 i 的前一个结点位置
int j = 0;
while(j < i)
{
r = r->next;
j++;
}
// 创建新节点
LNode *p = new LNode;
p->data = e;
// 实现 ①
p->next = r->next;
// 实现 ②
r->next = p;
}
int main()
{
LinkList L;
cout << "请输入单链表的长度:";
// n 表示单链表的长度
int n;
cin >> n;
cout << "请输入链表中元素: ";
// 测试头插法创建单链表
// createHead(L, n);
// 测试尾插法创建单链表
createTail(L, n);
// 遍历链表
// getElementAll(L);
// 测试根据下标获取元素值
// cout << "请输入下标:";
int i, e;
// cin >> i;
// getElement(L, i, e);
// cout << "下标" << i << "的值为:" << e << endl;
// 测试给定值获取结点
// cout << "请输入值:";
// cin >> e;
// LNode *p = LocationElement(L, e);
// if(p)
// {
// cout << "此结点值为: " << p->data;
// }
// else
// {
// cout << "此结点不存在!";
// }
// 测试 根据下标插入新结点
cout << "请输入下标: ";
cin >> i;
cout << "请输入被插入结点的值:";
cin >> e;
insertElement(L, i, e);
getElementAll(L);
return 0;
}
测试结果:
删除
根据给定下标删除结点
代码实现:
/**
* 根据给定下标删除结点
*
* @param &L 表示指向头结点的头指针
* @param i 表示下标(下标从 0 开始)
*
* */
void deleteElement(LinkList &L, int i)
{
LNode *p = L->next;
int j = 0;
// 实现 ①
while(j < i)
{
p = p->next;
j++;
}
// 实现 ②
LNode *q = p->next;
p->next = q->next;
// 实现 ③
delete q;
}
测试删除
完整代码:
#include <iostream>
using namespace std;
typedef struct LNode
{
// 这里数据域使用 int 型, 这里使用了递归定义
int data;
LNode *next;
}LNode, *LinkList;
/**
* 使用头插法创建单链表,注意是逆置创建
*
* @param &L 指向头结点的头指针
* @param n 表示插入结点的个数
*
* */
void createHead(LinkList &L, int n)
{
// 首先将头结点的指针域置空
L = new LNode;
L->next = NULL;
for(int i = 0; i < n; i++)
{
// 生成新节点
LNode *p = new LNode;
// 输入新节点中的 data
cin >> p->data;
// 实现图解 ①
p->next = L->next;
// 实现图解 ②
L->next = p;
}
}
/**
* 尾插法创建单链表
*
* @param &L 表示指向头结点的头指针
* @param n 表示要插入结点个数
*
* */
void createTail(LinkList &L, int n)
{
L = new LNode;
L->next = NULL;
// 首先让指针 r 指向最后一个结点
LNode *r = L;
for(int i = 0; i < n; i++)
{
LNode *p = new LNode;
// 实现第 ① 步
p->next = NULL;
cin >> p->data;
// 实现第 ② 步
r->next = p;
// 实现第三步
r = p;
}
}
/**
* 遍历单链表
*
* @param &L 表示指向头结点的头指针
*
* */
void getElementAll(LinkList &L)
{
LNode *p = L->next;
while(p != NULL)
{
cout << p->data << " ";
p = p->next;
}
}
/**
* 取值(根据序号取值)
*
* @param &L 表示指向头结点的头指针
* @param i 表示输入的序号
* @param &e 通过序号 i 查找到的值,传递回去
*
* */
void getElement(LinkList &L, int i, int &e)
{
LNode *p = L;
int j = 0;
// 这里 j <= i 的时候表示和数组下标相同,都是从 0 开始
// 如果写成 j < i,表示从 1 开始
// 为了符合数组从 0 开始的习惯,这里写成 j <= i
while((p != NULL) && j <= i)
{
p = p->next;
j++;
}
e = p->data;
}
/**
* 根据给定值查找结点
*
* @param L 表示指向头结点的头指针
* @param e 表示给定值
* @return 返回值是结点的地址,如果不存在返回 NULL
*
* */
LNode *LocationElement(LinkList L, int e)
{
// 首先 p 指向首元结点
LNode *p = L->next;
while(p)
{
if(p->data == e)
{
// 查找成功,返回地址
return p;
}
p = p->next;
}
// 查找失败,返回NULL,此时 p->next == NULL;
return p;
}
/**
* 根据下标,在该下标前插入新结点
*
* @param &L 表示指向头结点的头指针
* @param i 表示下标,这里下标从 0 开始,和数组一样,便于理解
* @param e 表示要插入的新结点的 data
*
* */
void insertElement(LinkList &L, int i, int e)
{
// 首先 r 指向头结点
LNode *r = L;
// j 用于定位指向 i 的前一个结点位置
int j = 0;
while(j < i)
{
r = r->next;
j++;
}
// 创建新节点
LNode *p = new LNode;
p->data = e;
// 实现 ①
p->next = r->next;
// 实现 ②
r->next = p;
}
/**
* 根据给定下标删除结点
*
* @param &L 表示指向头结点的头指针
* @param i 表示下标(下标从 0 开始)
*
* */
void deleteElement(LinkList &L, int i)
{
LNode *p = L->next;
int j = 0;
// 实现 ①
while(j < i)
{
p = p->next;
j++;
}
// 实现 ②
LNode *q = p->next;
p->next = q->next;
// 实现 ③
delete q;
}
int main()
{
LinkList L;
cout << "请输入单链表的长度:";
// n 表示单链表的长度
int n;
cin >> n;
cout << "请输入链表中元素: ";
// 测试头插法创建单链表
// createHead(L, n);
// 测试尾插法创建单链表
createTail(L, n);
// 遍历链表
// getElementAll(L);
// 测试根据下标获取元素值
// cout << "请输入下标:";
int i, e;
// cin >> i;
// getElement(L, i, e);
// cout << "下标" << i << "的值为:" << e << endl;
// 测试给定值获取结点
// cout << "请输入值:";
// cin >> e;
// LNode *p = LocationElement(L, e);
// if(p)
// {
// cout << "此结点值为: " << p->data;
// }
// else
// {
// cout << "此结点不存在!";
// }
// 测试 根据下标插入新结点
// cout << "请输入下标: ";
// cin >> i;
// cout << "请输入被插入结点的值:";
// cin >> e;
// insertElement(L, i, e);
// getElementAll(L);
// 测试 删除结点
cout << "请输入下标: ";
cin >> i;
deleteElement(L, i);
getElementAll(L);
return 0;
}
测试结果: