Files
wangdao-data-structure/ch2/single-link/README.md
2020-11-29 19:47:37 +08:00

537 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 单链表
## 1. typedef 关键词
`typedef` 关键字:数据类型重命名。
```cpp
typedef <> <>
```
```cpp
int x = 1;
int *p;
```
```cpp
typedef int zhengshu;
typedef int *zhengshuzhizhen;
zhengshu x = 1;
zhengshuzhizhen p;
```
## 2. 定义单链表
结点:
- 数据域
- 指针域
```cpp
struct LNode
{
ElemType data;
struct LNode *next;
};
```
```cpp
struct LNode *p = (struct LNode*) malloc(sizeof(struct LNode))
```
```cpp
typedef struct LNode LNode;
LNode *p = (LNode *) malloc(sizeof LNode);
```
高级写法:
```cpp
typedef struct LNode
{
ElemType data;
struct LNode *next;
} LNode, *LinkList;
```
```cpp
struct LNode
{
ElemType data;
struct LNode *next;
};
typedef struct LNode LNode;
typedef struct LNode *LinkList;
```
声明一个单链表时,只需要声明一个头指针 `L`,指向单链表的第一个结点。
```cpp
LNode *L; // 强调这是一个结点
```
```cpp
LinkList L; // 强调这是一个单链表
```
## 3. 不带头结点的单链表
```cpp
// 初始化一个空的单链表,不带头结点
bool InitList(LinkList &L)
{
L = NULL;
return true;
}
```
```cpp
// 判断单链表是否为空
bool Empty(LinkList L)
{
return L == NULL;
}
```
写代码更麻烦:
- 对第一个数据结点和后续数据结点的处理需要用不同的代码逻辑。
- 对空表和非空表的处理需要用不同的代码逻辑。
## 4. 带头结点的单链表
头结点:
- `data` 没有数据
- `next` 指向第一个结点,空表时为 `NULL`
```cpp
// 初始化一个空的单链表,带头结点,头结点不存储数据
bool InitList(LinkList &L)
{
// 分配一个头结点
L = (LNode *)malloc(sizeof(LNode));
if (L == NULL)
{
return false;
}
L->next = NULL;
return true;
}
```
```cpp
// 判断单链表是否为空
bool Empty(LinkList L)
{
return L->next == NULL;
}
```
写代码更方便。
## 5. 按位序插入
### 5.1. 带头结点
`ListInsert(&L,i,e)` 步骤:
1. 找到第 $i-1$ 个结点 `p`
2. 申请一个结点 `s`,存入数据元素 `e`
3. 修改 `s` 的后继结点为 `p` 的后继结点
4. 修改 `p` 的后继结点为 `s`
当插入位置 $i=1$ 时,可以把头结点看作为第 $0$ 个结点,以上逻辑同上适用。
```cpp
// 在第 i 个位置插入元素 e带头结点
bool ListInsert(LinkList &L, int i, int e)
{
// i 的值必须是合法的位序
if (i < 1)
{
return false;
}
LNode *p;
int j = 0; // 当前 p 指向的是第几个结点
p = L; // 指向第 0 个结点
while (p != NULL && j < i - 1) // 循环找到第 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;
}
```
- 最好时间复杂度:$O(1)$
- 最坏时间复杂度:$O(n)$
- 平均时间复杂度:$O(n)$
### 5.2. 不带头结点
插入位序 $i=1$ 时的步骤:
1. 申请一个结点 `s`,存入数据元素 `e`
2. 修改 `s` 的后继结点为 `L` 指向的结点
3. 修改 `L` 指向结点 `s`
插入位序 $i>1$ 时的步骤:
```cpp
// 在第 i 个位置插入元素 e不带头结点
bool ListInsert(LinkList &L, int i, int e)
{
// i 的值必须是合法的位序
if (i < 1)
{
return false;
}
// 插入第 1 个结点的操作与其他结点的操作不同
if (i == 1)
{
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = L;
L = s;
return true;
}
LNode *p;
int j = 1; // 当前 p 指向的是第几个结点
p = L; // 指向第 0 个结点
while (p != NULL && j < i - 1) // 循环找到第 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;
}
```
## 6. 指定结点的后插操作
1. 申请一个结点 `s`,存入数据元素 `e`
2. 修改 `s` 的后继结点为 `p` 的后继结点
3. 修改 `p` 的后继结点为 `s`
```cpp
// 后插操作:在结点 p 后插入元素 e
bool InsertNextNode(LNode *p, int 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;
}
```
时间复杂度:$O(1)$
## 7. 指定结点的前插操作
思路一:
```cpp
bool InsertPriorNode(LinkList L, LNode *p, int e);
```
- 需要传入头结点
- 时间复杂度:$O(n)$
**思路二**
1. 在结点 `p` 后插入一个结点 `s``s` 的数据元素为 `e`
2. 修改 `s` 的数据元素为 `p` 的数据元素
3. 修改 `p` 的数据元素为 `e`
- 不需要传入头结点
- 时间复杂度:$O(1)$
```cpp
// 前插操作:在结点 p 前插入元素 e
bool InsertPriorNode(LNode *p, int e)
{
if (p == NULL)
{
return false;
}
LNode *s = (LNode *)malloc(sizeof(LNode));
s->next = p->next;
p->next = s;
s->data = p->data;
p->data = e;
return true;
}
```
```cpp
// 前插操作:在结点 p 前插入结点 s
bool InsertPriorNode(LNode *p, LNode *s)
{
if (p == NULL || s == NULL)
{
return false;
}
s->next = p->next;
p->next = s;
int temp = s->data;
s->data = p->data;
p->data = temp;
return true;
}
```
## 8. 按位序删除(带头结点)
`ListDelete(&L,i,&e)` 步骤:
1. 找到第 $i-1$ 个结点 `p` 和被删除结点 `q`
2. 修改 `e` 的值为 `q` 的数据元素值
3. 修改 `p` 的后继结点为 `q` 的后继结点
4. 释放 `q`
```cpp
// 按位序删除结点
bool ListDelete(LinkList &l, int i, int &e)
{
// i 的值必须是合法的位序
if (i < 1)
{
return false;
}
LNode *p;
int j = 0; // 当前 p 指向的是第几个结点
p = L; // 指向第 0 个结点
while (p != NULL && j < i - 1) // 循环找到第 i-1 个结点
{
p = p->next;
j++;
}
if (p == NULL) // i 值不合法
{
return false;
}
if (p->next == NULL) // 第 i-1 个结点后已无其他结点
{
return false;
}
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}
```
- 最好时间复杂度:$O(1)$
- 最坏时间复杂度:$O(n)$
- 平均时间复杂度:$O(n)$
## 9. 指定结点删除
类似于前插操作,`DeleteNode(LNode *p)` 的步骤:
1. 声明一个指针 `q` 指向结点 `p` 的后继结点
2. 修改 `p` 的数据元素为 `q` 的数据元素
3. 修改 `p` 的后继结点为 `q` 的后继结点
4. 释放 `q`
> 但是如果结点 p 是表尾结点,此方法将不适用,因为找不到 p 的后继结点。只能从表头开始查找并删除。
## 10. 单链表的局限性
无法逆向检索,有时候不太方便。
- 带头结点和不带头结点的区别
- “封装”的好处
## 11. 按位查找(带头结点)
```cpp
// 按位查找:返回第 i 个元素,带头结点,头结点看作是第 0 个结点
LNode *GetElem(LinkList L, int i)
{
if (i < 0)
{
return NULL;
}
LNode *p;
int j = 0; // 当前 p 指向的是第几个结点
p = L; // 指向第 0 个结点
while (p != NULL && j < i) // 循环找到第 i-1 个结点
{
p = p->next;
j++;
}
return p;
}
```
- 时间复杂度:$O(n)$
## 12. 按值查找(带头结点)
```cpp
// 按值查找,找到数据域 == e 的结点
int LocateElem(LinkList L, int e)
{
LNode *p = L->next;
// 从第 1 个结点开始查找数据域为 e 的结点
while (p != NULL && p->data != e)
{
p = p->next;
}
// 找到后返回该节点指针,否则返回 NULL
return p;
}
```
- 时间复杂度:$O(n)$
## 13. 求表的长度
```cpp
// 求表的长度
int Length(LinkList L)
{
int len = 0;
LNode *p = L;
while (p->next != NULL)
{
p = p->next;
len++;
}
return len;
}
```
- 时间复杂度:$O(n)$
## 14. 单链表的建立
单链表的建立步骤:
1. 初始化一个单链表
2. 每次取一个数据元素,插入到表尾/表头
### 14.1. 尾插法
思路一:
1. 初始化单链表
2. 设置变量 `length` 记录链表长度
3. `while` 循环:
1. 每次取一个数据元素 `e`
2. `ListInsert(L, length+1, e)` 插到尾部
3. `length++`
- 时间复杂度:$O(n^2)$
**思路二**
1. 初始化单链表
2. 设置指针 `r` 指向表尾元素
3. `while` 循环:
1.`r` 之后插入结点 `s`
2. 修改 `r` 指向 `s`
```cpp
// 尾插法建立单链表
LinkList List_TailInsert(LinkList &L)
{
int x;
L = (LNode *)malloc(sizeof(LNode));
LNode *s, *r = L; // r 为表尾指针
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;
}
```
- 时间复杂度:$O(n)$
### 14.2. 头插法
1. 初始化单链表
2. `while` 循环:
1. 每次取一个数据元素 `e`
2. `InsertNextNode(L, e)`
```cpp
// 头插法建立单链表
LinkList List_HeadInsert(LinkList &L)
{
int x;
L = (LNode *)malloc(sizeof(LNode));
L->next = NULL; // 一定要初始化头结点的 next
LNode *s;
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(n)$
> 养成好习惯:只要是初始化单链表,就先把头指针指向 NULL。
头插法的重要应用:链表的逆置。
```cpp
LinkList List_Reverse(LinkList &L)
{
LinkList L2 = (LNode *)malloc(sizeof(LNode));
L2->next = NULL;
LNode *s = L->next;
while (s != NULL)
{
LNode *s2 = (LNode *)malloc(sizeof(LNode));
s2->data = s->data;
s2->next = L2->next;
L2->next = s2;
s = s->next;
}
return L2;
}
```