数据结构 DATA STRUCTURE
二、线性表
2.1 线性表的定义和基本操作概述
2.2 线性表的顺序表示
推荐阅读:顺序表的定义和基本操作的实现
2.3 线性表的链式表示
2.3.1 单链表
一、单链表存储结构描述和特点
线性表的链式存储,通常用指针来描述线性表的链式存储。
通过一组任意的存储单元来存储线性表中的数据元素,通过“链”建立起数据元素之间的关系——为建立数据元素之间的 线性关系,对每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。
1. 单链表存储结构描述
typedef struct LNode { // 定义单链表结点类型(链表就是一个个结点通过结点的指针域连起来)
Elemtype data; // 数据域
struct LNode * next; // 指针域,注意这里定义为 struct LNode *类型,因为还没重命名(不能直接用LNode*)。
}LNode,* LinkList;
为什么要用 typedef?
结构体使用方法:
struct LNode L;
重命名之后使用方法:
LNode *L;
或LinkList L;
使用更加方便。
为什么要用两个重命名(LNode 和 LinkList),有什么区别?
拆开看:
typedef struct LNode { Elemtype data; struct LNode *next; }LNode;
重命名
struct LNode
结构体类型为LNode
。typedef struct LNode { Elemtype data; struct LNode *next; }* LinkList;
重命名
struct LNode*
结构体指针类型为LinkList
。使用方法不一样:
LNode *L;
和LinkList L;
但作用完全相同,后者可读性更强。(用于强调——前者强调这是个结点,后者强调这是一个表。)
2. 单链表特点
优:
- 不需要使用地址连续的存储单元(不要求逻辑上相邻的元素在物理位置上也相邻)
- 插入删除操作快(不需要移动元素,修改指针即可)
缺:
- 不能随机存取,只能顺序存取。(每一个结点只认识后继结点)
- 附加指针域,浪费空间。
3. 单链表的一些概念
头指针:用来标识单链表,值为NULL时表示为空表。
头结点:为了操作上的方便,在单链表第一个结点之前附加一个结点。头结点的数据域可以不设任何信息,也可以记录表长等信息。
头指针和头结点区分:不管带不带头结点,头指针都始终指向链表的第一个结点;而头结点是带头结点的链表中的第一个结点,结点内通常不存储信息。
引入头结点优点:
- 使链表在第一个位置上的操作和在表的其他位置上的操作一致,无需特殊处理。
- 使空表和非空表的处理统一。
二、单链表基本操作实现
1. 创建单链表(初始化)
(1)头插法
从空表开始,建立头结点,然后循环将新结点插入到表头(头结点的后一个位置)来创建单链表
Steps:
- 创建头结点
- 一直在且只在头结点和第一个结点之间的位置插入结点
// 头插法建立单链表
LinkList List_HeadInsert(LinkList &L) {
// Variables Definition
LNode *s; // 中间变量,临时保存结点
int x; // 中间变量,临时保存输入的数据
// Create HeadNode
L = (LinkList)malloc(sizeof(LNode)); // 创建头结点(由系统生成一个LNode型的结点,同时将该结点的起始位置赋给指针变量s。)
// Initialization
L->next = NULL; // 初始化为空链表
// Input
printf("Input: \n");
scanf("%d", &x);
// 头插 (在头结点后一位置一直插入,也是当前结点的前一个位置)
while (x != 9999) { // Input 9999 means the end
s = (LNode *)malloc(sizeof(LNode)); // 1. 指针需要动态分配存储
// Assignment
s->data = x; // 2. 先将input的x赋值到结点
// Insert
s->next = L->next; // 3. 先接管人家儿子
L->next = s; // 4. 再做人家新的儿子
// Next Input
scanf("%d", &x);
}
// Return
return L; // 返回生成的单链表
}
Questions:
-
s->data
什么意思?s->data
==(*s).data
时间复杂度:每个结点插入的时间为 O(1),总时间复杂度为O(n)。
(2)尾插法
增设尾指针 r,循环将新结点插入到表尾并更新尾指针 来创建单链表。
Steps:
- 创建头结点,并设置当前指针s,尾指针r
- 循环创建结点并接到r后面。
- 更新r(将r指向新的尾结点)
- 尾结点指针置空
// 尾插法建立单链表
LinkList List_TailInsert(LinkList &L) {
// Variables Definition
int x;
L = (LinkList)malloc(sizeof(LNode));
LNode *s, *r = L; // r--tailpointer
// Input
printf("Input: \n");
scanf("%d", &x); // Input
// TailInsert
while (x != 9999) {
s = (LNode *)malloc(sizeof(LNode)); // Create Temporary-Node
// Assignment
s->data = x;
// Insert(Link to the Temp node)
r->next = s;
// Move the tailpointer
r = s;
// Next Input
scanf("%d", &x);
}
r->next = NULL;
// Return
return L;
}
Summary:
头插 VS 尾插
因为是后插(尾插用的就是只在尾巴后插)操作,直接
r->next = s;
即可,比前插简单多了。(其实也可以s->next = r->next
,但是没必要。因为最后直接进行尾结点指针置空(r->next = NULL
)就行,对所有要后插的结点来说,都是这样,所以,等插入完毕之后,来一句r->next = NULL
就OK了。)(就像有素质的在最后面排队一样)而头插就不行了,因为他们后面的结点是随时更新的。(就像没素质的插队一样)
顺序表 VS 链表
为什么前面讲的顺序表没有这样边输入边创建?
那是因为本来顺序表就是提前申请一片连续空间来创建的,不会像这样”边输入边创建“呀。
你非要输入进行初始化的话,设置个变量直接存进L.data[i]就行(而链表就需要不断移动指针,然后p->data)。(没错,链表的操作就是比顺序表繁琐一丢丢。但是换来的是插入和删除场景下时间上的提高。)
时间复杂度:每个结点插入的时间为 O(1),总时间复杂度为O(n)。
(3)根据 传入数组 初始化单链表,用尾插法实现
将尾插法中的“输入并赋值”部分,改为赋值为传入的数组。
Steps:
- 为定义的单链表L分配空间,创建头结点
- 设置一个尾指针,初始指向头结点
- 根据传进来的参数len,循环创建新结点p,并将p->data赋值为Arr[i]
- 将p链接到r屁股后面
- 最后将尾结点指针置空
// 赋值(根据传入数组尾插法“创建”单链表)
void ListAssign(LinkList &L, ElemType Arr[], int len) { // 由于形参数组只是个指针,无法传送求数组长度信息,所以只能添加数组长度参数
L = (LNode *)malloc(sizeof(LNode));
int i;
LNode *r = L; // 尾指针
for (i = 0; i < len; i++) {
LNode *p = (LNode *)malloc(sizeof(LNode));
p->data = Arr[i]; // 赋值
r->next = p; // 将 p 链接到 尾指针
r = p; // 移动尾指针
}
r->next = NULL; // 尾结点指针置空
}
Questions:为什么要
L = (LNode *)malloc(sizeof(LNode));
Answer:传入的 L 只是用
LinkList L;
定义了一下,也只是个指针,一个没有内容的指针,所以在初始化的时候要先L = (LNode *)malloc(sizeof(LNode));
进行赋值奥。
(4)初始化为空表
分配头结点,并且设置头结点指针域为空。
// 初始化空单链表
bool ListInit(LinkList &L) {
L = (LNode *)malloc(sizeof(LNode)); // 分配一个头结点(不带头结点的话不用这个 )
if (L == NULL) // 内存不足分配失败
return false;
L->next = NULL;
return true;
}
2. 插入操作
在表 L 的 i 位置插入 e。
Steps:
先检查插入位置的合理性,然后找到待插入结点的前驱节点(
i-1
),最后插入新结点。
- 检查 i 是否合理
- 获取第 i-1 个结点的指针
- 创建新结点,并且将 e 存放到结点的数据域
- 插入
- 接管 第i-1个结点 的儿子 :
s->next = p->next;
- 成为 第i-1个结点 的儿子 :
p->next = s;
- return
// 插入
bool ListInsert(LinkList &L, int i, ElemType e) {
// Judge whether i is valid
if ((i < 1) || (i > Length(L)+1))
return false;
// Get the i-1 node
LNode *p = GetElem(L, i-1); // 调用按序查找函数
// Create Temporary-Node and Assign INPUT to s->data
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = e;
// Insert
s->next = p->next;
p->next = s;
// return
return true;
}
插入操作总是先拿到没有“直接指针”的结点,然后再去搞那些有“直接指针”的。(防止没有“直接指针”的结点丢失。)
3. 删除操作
删除表 L 第 i 位置元素,并将其存放到全局变量e。
Steps:
- 检查 i 是否合理
- 获取第 i-1 个结点的指针
- 将 i 结点的值 p->next->data 放到 e
- 删除 i 结点:令 第i-1结点 的next指针指向 第i+1结点(i->next) 即可。
get 第 i-1 个结点,然后拿到第 i 个结点,最后让 i-1 结点直接指向 i 的 next ,也就是 i+1 。
// 删除
bool ListDelete(LinkList &L, int i, ElemType &e) {
if (i<1 || i > Length(L))
return false;
LNode *p = GetElem(L, i-1);
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}
Q:为什么有的函数里的LNode指针需要malloc动态分配存储,有的不需要?
A:只有涉及到创建结点的时候需要malloc(像创建、赋值等)。但是像查找、删除等需要用到 LNode指针的情况,就不需要malloc了,因为直接指向目标存储就OK了,这个存储是之前申请过的啦(一般的话,之前用初始化/赋值/创建函数申请了。),直接指向它即可。
例:
LNode *p = GetElem(L, i-1); // 这个获取第i-1个结点,这个结点之前已经申请了空间了! LNode *s = (LNode*)malloc(sizeof(LNode)); // 而 s 这个中间结点,是没有空间的!需要新申请!
4. 按序查找
获取表 L 第 i 个结点的指针。
Steps:
- i 的合法性判断
- 初始化计数器、拿到第一个节点
- 只要 p(当前结点) 不为空,且 计数器 < i ,就循环
p->next
到j-1
。(把自己想if的东西可以直接放到循环里:if(j=i)
)- 如果 j>=i ,会返回p->next(最后一个结点的next指针),为NULL。
// 按序号查找结点
LNode *GetElem(LinkList L, int i) {
int j = 1; // counter
// Assign the FirstNode's pointer to p(TempPoiter)
LNode *p = L->next; // FirstNode, not HeadNode
// Judge whether i is valid
if (i == 0)
return L;
if (i < 1)
return NULL;
// Search
while (p && j < i) { // 如果 p(当前结点) 不为空,且 计数器 < i
p = p->next;
j++;
}
// Return the pointer(previousNode's pointer domain) of ith node
return p; // if j >= i,then NULL(lastNode's pointer domain) will be returned.
}
Q:为什么不直接返回值?而是一个结点?
A:因为GetElem函数返回结点的话,不仅能返回值,还能让插入,删除函数使用。(因地制宜,按需设计)
5. 按值查找
获取表 L 中值为 e 的结点的指针。
Steps:
循环找下一个是不是
e
,循环最后没找到返回的p
的指针域就是NULL
。
- 拿到第一个结点
- 只要 p 不为空 且 p->data 不等于 e,就循环 p=p->next
- 返回 p
// 按值查找
LNode *LocateElem(LinkList L, ElemType e) {
LNode *p = L->next;
while (p != NULL && p->data != e)
p = p->next;
return p;
}
6. 判空
判断头结点的指针域是否为空。
// 判空
bool Empty(LinkList L) {
// if (L->next == NULL)
// return true;
// else
// return false;
return (L->next == NULL); // 一句式写法,666
}
7. 求表长
求表 L 的长度。
Steps:用计数器变量计数,p不为空则一直 next。
// 求表长
int Length(LinkList L) {
int len = 0;
LNode *p = L->next;
while (p) {
len++;
p = p->next;
}
return len;
}
8. 打印表
Steps:循环 printf,并且 next。
// 打印
void ListPrint(LinkList L) {
LNode *p = L->next;
while(p)
printf("%d ", p->data);
printf("\n");
}
9. Else
1. 前插
在给定结点 p 前插入结点 s。
单链表的算法中,通常采用 后插 操作。前插操作的话,需要找到前驱结点,再进行插入,时间复杂度高 O(n)。那有没有一种O(1)的前插算法呢?
Steps:后插,然后交换数据域。(“偷天换日”)
// 后插
s->next = p->next;
p->next = s;
// 交换数据域
temp = p->data;
p->data = s->data;
s->data = temp;
2. 删除指定结点
删除给定结点 p 。
找前驱结点,然后删除?O(n)——给的是待删除结点p,没有p的前驱结点,传统操作是删不了p的……
Steps:拿到后继结点的值,然后删除后继结点。(“找替死鬼”)
q = p->next;
p->data = p->next->data; // 拿到后继结点数据
p->next = q->next; // 抛弃后继结点
free(q);
单链表总结
-
LinkList
- 插入操作很核心
- 接管人家的儿子
- 做人家儿子
- 删除操作直接链接到后继结点的next。
- 头插尾插创建,都涉及到插入操作。
- 注意创建LinkList之前需要初始化(主要是让
L->next=NULL
)。
- 插入操作很核心
-
HeadInsert VS TailInsert
Item HeadInsert TailInsert 总体 头插麻烦一点,插入麻烦一丢丢 尾插简单,就是多设置一个尾指针,往后链接就OK。 时间复杂度 O(n) O(n) 输入数据顺序 和 生成的链表中的元素顺序 相反 相同 虽然结点插入时间复杂度是O(1),但是需要插入n次,故两个插入创建链表方式时间复杂度都是O(n),因为需要循环一个个赋值,(好像不管什数据结构,创建操作都是O(n)吧)
-
GetElem VS LocateElem
- 按值查找代码很少,少的就是关于 计数器变量 的部分。(计数器变量的定义,有效性判断以及使用)
- 时间复杂度都是 O(n)。(因为链表的数据结构,无法直接拿到目标,需要一个个查)
-
p=L
VSp=L->next
就是设置不同的起始点而已。
三、顺序表、单链表总结
一、选出最优数据结构:
-
存取第 i 个元素及其前驱和后继元素的值
顺序表 O(1)
-
交换第3个元素和第4个元素的值
顺序表 O(1)
-
顺序输出表内所有值
顺序表 = 单链表 = O(n)
-
给出指定点,在指定点前插/后插
单链表 O(1)
二、算法思想总结
-
写一个函数需要关注的点:参数及参数是否需要引用,返回值及返回值类型。
函数代码一般包括:
- 合法性判断
- 良好的return
- 核心逻辑算法
-
最小/大值一般都是定义一个变量保存 数组的第一个元素 为初始值,然后循环和剩下的比较并进行更新。
-
倒置这种对整个表所有元素的操作,一般处理一半表就可以了。处理一半表,就是循环到 L.length/2。
-
删除所有x,删除操作一般都要考虑空间复杂度,可以优化到 O(1) 。
三、顺序表 VS 链表
-
优缺点对比
顺序表 单链表 优 1. 随机存取:通过 首地址 和 元素序号 可在时间 O(1) 内找到指定的元素。
2. 存储密度高:每个结点只存储数据元素。1. 不需要使用地址连续的存储单元(不要求逻辑上相邻的元素在物理位置上也相邻)
2. 插入删除操作快(不需要移动大量元素,修改指针即可)缺 1. 静态分配数组拓展容量不方便,动态分配数组方式时间复杂度高。
2. 插入和删除操作需要移动大量元素。
3. 需要使用一整块地址连续的存储单元。(逻辑相邻的物理也相邻)1. 不能随机存取,只能顺序存取。(每一个结点只认识后继结点)
2. 附加指针域,浪费空间。 -
操作对比
Item SqList LinkList 算法设计 一般用 数组L.data[i]、for循环 和 i循环变量,以及L.length来进行算法设计; 一般用 指针、while循环、s->next、以及p/p->next是否==NULL进行算法设计;
(头结点不可或缺总会用到,而且没前驱结点什么也干不了,而且新建结点需要手动动态分配内存–malloc)插入 先挪开位置,后数组的赋值进行插入。 先赋值完数据域,后设置指针域进行连接。 删除 值的覆盖即删除(目标元素后所有元素前移1位) 让值在的结点脱链就是删除(越过待删结点链接到待删结点后面) 拿第i个元素 直接对数组进行操作。data[i]。 链表操作之前总是需要拿到第一个结点,然后一直next到 前驱结点。 在我看来,数组也是“二级数据结构”,也是一种数据类型的封装,所以顺序表本来就站在一个至高点上。
-
效率对比
Operations SqList LinkList 插入 O(n) O(n) 给定前驱结点的插入 O(n) O(1) 删除 O(n) O(n) 给定前驱结点的删除 O(n) O(1) 按值查找 O(n) O(n) 按序查找 O(1) O(n) 以上时间复杂度为对应操作的平均时间复杂度
单链表基本操作测试(可直接运行)
代码:
#include <stdio.h>
#include <stdlib.h>
/*
* 单链表的存储结构定义
*/
#define ElemType int // 定义线性表元素数据类型
typedef struct LNode{ // 定义单链表结点类型(链表就是一个个结点通过结点的指针域连起来)
ElemType data; // 数据域
struct LNode *next; // 指针域
}LNode, *LinkList; // LNode * = LinkList, 前者强调这是个结点,后者强调这是个链表,完全一样
/*
* 单链表的基本操作实现
*/
// 初始化空单链表 (分配头结点,并且设置头结点指针域为空。)
bool ListInit(LinkList &L) {
L = (LNode *)malloc(sizeof(LNode)); // 分配一个头结点(不带头结点的话不用这个 )
if (L == NULL) // 内存不足分配失败
return false;
L->next = NULL;
return true;
}
// 赋值 (根据传入数组尾插法“创建”单链表)
void ListAssign(LinkList &L, ElemType Arr[], int len) { // 由于形参数组只是个指针,无法传送求数组长度信息,所以只能添加数组长度参数
L = (LNode *)malloc(sizeof(LNode));
int i;
LNode *r = L; // 尾指针
for (i = 0; i < len; i++) {
LNode *p = (LNode *)malloc(sizeof(LNode));
p->data = Arr[i]; // 赋值
// p->next = r->next;
r->next = p; // 将 p 链接到 尾指针
r = p; // 移动尾指针
}
r->next = NULL; // 尾结点指针置空
}
// 判空 (判断头结点的指针域是否为空。)
bool Empty(LinkList L) {
// if (L->next == NULL)
// return true;
// else
// return false;
return (L->next == NULL);
}
// 头插法建立单链表(从空表开始,建立头结点,然后将新结点插入到表头--头结点之后)
LinkList List_HeadInsert(LinkList &L) {
// Variables Definition
LNode *s; // 中间变量,临时保存结点
int x; // 中间变量,临时保存输入的数据
// Create HeadNode
L = (LinkList)malloc(sizeof(LNode)); // 创建头结点(由系统生成一个LNode型的结点,同时将该结点的起始位置赋给指针变量s。)
// Initialization
L->next = NULL; // 初始化为空链表
// Input
printf("Input: \n");
scanf("%d", &x);
// 头插 (在头结点后一位置一直插入,也是当前结点的前一个位置)
while (x != 9999) { // Input 9999 means the end
s = (LNode*)malloc(sizeof(LNode)); // 1. 指针需要动态分配存储
// Assignment
s->data = x; // 2. 先将input的x赋值到结点
// Insert
s->next = L->next; // 3. 先接管人家儿子
L->next = s; // 4. 再做人家新的儿子
// Next Input
scanf("%d", &x);
}
// Return
return L; // 返回生成的单链表
}
// 尾插法建立单链表(将新结点插入到表尾,增设尾指针 r)
LinkList List_TailInsert(LinkList &L) {
// Variables Definition
int x;
L = (LinkList)malloc(sizeof(LNode));
LNode *s, *r = L; // r--tailpointer
// Input
printf("Input: \n");
scanf("%d", &x); // Input
// TailInsert
while (x != 9999){
s = (LNode *)malloc(sizeof(LNode)); // Create Temporary-Node
// Assignment
s->data = x;
// Insert(Link to the Temp node)
r->next = s;
// Move the tailpointer
r = s;
// Next Input
scanf("%d", &x);
}
r->next = NULL;
// Return
return L;
}
/*
对于链表的创建,头插是一直循环用s(固定位置不需要移动指针),尾插是设置尾指针r且一直移动r实现遍历
尾插法如果不设置尾指针的话,每次插入都需要遍历得到前去结点,时间复杂度为 O(n2)。
*/
// 插入(先检查插入位置的合理性,然后找到待插入结点的前驱节点(i-1),最后插入新结点)
bool ListInsert(LinkList &L, int i, ElemType e) {
// Judge whether i is valid
if ((i < 1) || (i > Length(L)+1))
return false;
// Get the i-1st node
LNode *p = GetElem(L, i-1);
// Create Temporary-Node and Assign INPUT to s->data
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = e;
// Insert
s->next = p->next;
p->next = s;
// return
return true;
}
不带头结点的单链表按位序插入
//if (i == 1) {
// LNode *s = (LNode*)malloc(sizeof(LNode));
// s->data = e;
// s->next = L; // 不带头结点的头部插入和其他地方插入不一样,没有前驱结点,需要移动L指针。
// L = s;
// return true;
//}
// 指定结点后插
bool InsertNextNode(LNode *p, ElemType e) {
if (p == NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if (s == NULL); // 内存分配失败
return false;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
// 指定结点的前插操作(偷天换日法)
bool InsertPriorNode(LNode *p, ElemType e) {
if (p == NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if (s == NULL)
return false;
s->next = p->next; // Insert
p->next = s;
s->data = p->data; // 将p中元素复制到s
p->data = e; // p中元素覆盖为e
return true;
}
// 删除 (get第i-1个结点,然后拿到第i个结点,最后让i-1结点直接指向i的next,也就是i+1)
bool ListDelete(LinkList &L, int i, ElemType &e) {
if (i<1 || i > Length(L))
return false;
LNode *p = GetElem(L, i-1);
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}
// 按序号查找结点(从头结点开始,p->next到i-1)(p=L->next版)
LNode *GetElem(LinkList L, int i) {
int j = 1; // counter
// Assign the FirstNode's pointer to p(TempPointer)
LNode *p = L->next; // FirstNode, not HeadNode
// Judge whether i is valid
if (i == 0)
return L;
if (i < 1)
return NULL;
// Search
while (j < i && p) { // 如果 p(当前结点) 不为空,且 计数器 < i ;while (p && j < i)--这样效率高一点,前者可读性更高
p = p->next;
j++;
}
// Return the pointer(previousNode's pointer domain) of ith node
return p; // if j >= i,then NULL(lastNode's pointer domain) will be returned.
}
/*
1. 为什么不返回值?而是一个结点?
因为GetElem函数返回结点的话,不仅能返回值,还能让插入函数和删除函数使用。(因地制宜、按需设计)
2. 如果p=L,那么需要j=0(j表示p指向的是第几个结点)。
*/
// 按序号查找(p=L版,个人觉得这个更改、更简洁)
LNode *GetElem_2(LinkList L, int i) {
if (i < 0)
return NULL;
LNode *p = L;
int j = 0;
while (p!=NULL && j<i) {
p = p->next;
j++;
}
return p;
}
// 按值查找(循环找下一个是不是e,循环最后没找到的话返回的p的指针域就是NULL)
LNode *LocateElem(LinkList L, ElemType e) {
LNode *p = L->next;
while (p != NULL && p->data != e)
p = p->next;
return p; // 循环最后没找到的话返回的尾结点p的指针域就是NULL
}
// 求表长(用计数器变量计数,一直next)
int Length(LinkList L) {
int len = 0;
LNode *p = L->next;
while (p) {
len++;
p = p->next;
}
return len;
}
// 打印 (循环输出,并且next)
void ListPrint(LinkList L) {
if (Empty(L))
printf("The List is empty.\n");
else {
LNode *p = L->next;
while(p) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
}
/*
* main进行测试
*/
int main()
{
// Definition
LinkList list1, list2, list3;
ElemType e, a[10]={1,2,3,4,5,6,7,8,9,10};
int i = 0;
/*********************Test*********************/
// Init2EmptyList
ListInit(list3);
ListPrint(list3);
// Create by array
ListAssign(list3, a, 10);
ListPrint(list3);
// HeadInsert&TailInsert Initialization
printf("HeadINSERT Initialization Beginning...\n");
list1 = List_HeadInsert(list1);
printf("HeadINSERT Initialization Finished.\n");
printf("TailINSERT Initialization Beginning...\n");
List_TailInsert(list2);
printf("TailINSERT Initialization Finished.\n");
// ListPrint
ListPrint(list1);
ListPrint(list2);
// LengthPrint
printf("list1's length: %d\n", Length(list1));
printf("list2's length: %d\n", Length(list2));
// Insert
printf("LISTINSERT Beginning...\n");
printf("Input the Position and Value you want INSERT:\n");
scanf("%d%d", &i, &e);
ListInsert(list1, i, e);
printf("LISTINSERT Finished.\n");
ListPrint(list1);
// Delete
printf("DELETE Beginning...\n");
printf("Input the value\'s location you want DELETE:\n");
scanf("%d", &i);
ListDelete(list1, i, e);
ListPrint(list1);
// Read
printf("GET Beginning...\n");
printf("Input the value\'s location you want GET:\n");
scanf("%d", &i);
LNode *p = GetElem(list1, i);
printf("The %dthNode is %d\n", i, p->data);
printf("LOCATE Beginning...\n");
p = LocateElem(list1, 666);
if (p)
printf("666 is in List.\n");
else
printf("666 is not in List.\n");
/*********************End**********************/
// Return
return 0;
}
运行结果:
2.3.2 双链表
一、双链表存储结构描述和特点
存储结构描述:
typedef struct DLNode {
ElemType data;
struct DLNode *prior, *next; // 前驱和后继指针(都要带*,Type*不支持同时定义多个变量,除非先typedef)
}DLNode, * DLinkList;
特点:
增设前驱指针,解决单链表只能处理后继结点和必须从头遍历的痛点。
二、双链表基本操作实现
将“去”、“来”看成单链表的一次指向操作。
→+← == →/←来简化记忆。
1. 插入
(1)给定两结点前一结点 *p ,在两结点中间插入结点 s
// "右边"
s->next = p->next;
p->next->prior = s;
// "左边"
s->prior = p;
p->next = s; // 最后再修改p的next指针域,比较保险!
先处理没有直接指针的一边(此题是右边),再去搞有直接指针操控的一边(此题是左边),防止没有直接指针的一边丢失。
(2)给定两结点后一结点 *p 的插入,在两结点中间插入结点 s
// 左边
s->prior = p->prior;
p->prior->next = s;
// 右边
s->next = p;
p->prior = s;
2. 删除
删除 结点*p 的后继结点 *q。(给了两个结点!)
// 将 q 脱链——越过q,直接链上q->next
p->next = q->next;
q->next->prior = p;
// 释放q
free(q);
双链表基本操作实现
/*
* 双链表基本操作实现
*/
#include <stdio.h>
#include <stdlib.h>
// 双链表数据结构声明
typedef int ElemType;
typedef struct DNode {
ElemType data;
struct DNode *prior, *next;
}DNode, * DLinkList;
// 初始化
bool InitDLinkList(DLinkList &L) {
L = (DNode *)malloc(sizeof(DNode));
if (L == NULL)
return false;
L->prior = NULL;
L->next = NULL;
return true;
}
// 判空
bool Empty(DLinkList L) {
if (L->next == NULL)
return true;
else
return false;
}
// 在p结点后插入s结点
bool InsertNextDNode(DNode *p, DNode *s) {
// Validity Judgement
if (p==NULL || s==NULL)
return false;
// Insert
s->next = p->next;
if (p->next != NULL) // p为尾结点的话,p->next为NULL,没有prior
p->next->prior = s;
s->prior = p;
p->next = s;
// Return
return true;
}
/*
核心代码:
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;
*/
// 删除p结点的后继结点
bool DeleteNextDNode(DNode *p) {
if (p == NULL)
return false;
DNode *q = p->next;
if (q == NULL) // p没有后继
return false;
p->next = q->next;
if (p->next != NULL) // q结点不是尾结点
q->next->prior = p;
free(q);
return true;
}
/*
核心代码:
p->next = q->next;
q->next->prior = p;
free(q);
*/
// 销毁
bool DestoryList(DLinkList &L) {
while (L->next != NULL) // 遍历删除结点
DeleteNextDNode(L);
free(L); // 释放头结点
L = NULL;
return true;
}
// 打印
bool ListPrint(DLinkList L) {
DLNode *p = L->next;
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
return true;
}
/*
遍历:
// 后向遍历
while (p != NULL) {
p = p->next;
}
// 前向遍历
while (p != NULL) {
p = p->prior;
}
// 前向遍历(跳过头结点)
while (p->prior != NULL) {
p = p->prior;
}
*/
/*
Summary
1. 非循环链表中很多对表尾的插入删除操作都会出现没有前驱问题(p->prior出错),所以在执行对应操作的时候总要进行一些合法性判断,而循环链表则不用担心。
*/
2.3.3 循环链表
一、循环单链表
尾结点指针不是NULL,而指向头结点。(注意不是第一个元素的结点,是头结点!为了方便操作统一而设置的结点。)
判空条件:头结点是否指向本身(头结点是否等于头指针)
if (L->next == L)
特点:
-
经常在表头表尾进行操作的话,可以用 带尾指针的循环单链表。
-
可以从表中任意结点开始遍历整个链表。
二、循环双链表
尾结点指向头结点,而且头结点的前驱指向尾结点,尾结点的后继指向头结点。
判空:if ((L->next == L) && (L->prior == L))
2.3.4 静态链表
借助数组来实现链式存储,指针域存储下个结点的相对地址(数组下标)。
和顺序表一样,需要预先分配一块连续的内存空间。
#define MaxSize 50
typedef struct {
ElemType data; // 数据
int next; // 下一个元素的相对地址(数组下标)
}SLinkList[MaxSize];
特点:
next == -1
为结束标志。- 插入、删除等操作与动态链表一样,只需要修改指针,不需要修改或移动元素。
静态链表没有单链表使用方便,但是在不支持指针的高级语言(如Basic)中,这是一种非常巧妙的设计方法。
回顾一下吧: