完成单链表

This commit is contained in:
lifei
2020-11-29 19:47:37 +08:00
parent 925f66470c
commit fc8d9fea74
7 changed files with 994 additions and 36 deletions

View File

@@ -138,23 +138,7 @@ $$
算法的时间复杂度(**常对幂指阶**
$$
O(1)
\lt
O(log_2n)
\lt
O(n)
\lt
O(nlog_2n)
\lt
O(n^2)
\lt
O(n^3)
\lt
O(2^n)
\lt
O(n!)
\lt
O(n^n)
O(1) < O(log_2n) < O(n) < O(nlog_2n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
$$
- 结论一顺序执行的代码只会影响常数项可以忽略
@@ -229,23 +213,7 @@ $$
常对幂指阶
$$
O(1)
\lt
O(log_2n)
\lt
O(n)
\lt
O(nlog_2n)
\lt
O(n^2)
\lt
O(n^3)
\lt
O(2^n)
\lt
O(n!)
\lt
O(n^n)
O(1) < O(log_2n) < O(n) < O(nlog_2n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
$$
## 3. [线性表](ch2/README.md)

View File

@@ -56,7 +56,12 @@ Tips
## 4. [顺序表](sequence/README.md)
## 5. 单链表
- 优点:可随机存取,存储密度高。
- 缺点:要求大片连续空间,改变容量不方便。
## 5. [单链表](single-link/README.md)
- 缺点:无法逆向检索,有时候不太方便。
## 6. 双链表

View File

@@ -89,7 +89,7 @@ C++ 可以使用 `new``delete` 关键字。
## 3. 顺序表的特点
- **随机访问**:即可以在 $O(1)$ 时间内找到第 $i$ 个元素。
- 存储密度高:每个点只存储数据元素。
- 存储密度高:每个点只存储数据元素。
- 扩展容量不方便:静态分配不能扩展容量,动态分配可以扩展但时间复杂度高。
- 插入、删除元素操作不方便,需要移动大量元素。

536
ch2/single-link/README.md Normal file
View File

@@ -0,0 +1,536 @@
# 单链表
## 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;
}
```

View File

@@ -0,0 +1,98 @@
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode
{
int data;
struct LNode *next;
} LNode, *LinkList;
bool InitList(LinkList &L)
{
L = (LNode *)malloc(sizeof(LNode));
if (L == NULL)
{
return false;
}
L->next = NULL;
return true;
}
// 尾插法建立单链表
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;
}
// 头插法建立单链表
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;
}
// 头插法实现单链表的逆置
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;
}
int main()
{
LinkList L;
List_TailInsert(L);
// List_HeadInsert(L);
// 打印操作
LNode *p;
p = L->next;
while (p != NULL)
{
printf("%d\n", p->data);
p = p->next;
}
// 测试单链表逆置
LinkList L2 = List_Reverse(L);
// 打印操作
p = L2->next;
while (p != NULL)
{
printf("%d\n", p->data);
p = p->next;
}
return 0;
}

224
ch2/single-link/with.cpp Normal file
View File

@@ -0,0 +1,224 @@
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode
{
int data;
struct LNode *next;
} LNode, *LinkList;
// 初始化一个空的单链表,带头结点,头结点不存储数据
bool InitList(LinkList &L)
{
// 分配一个头结点
L = (LNode *)malloc(sizeof(LNode));
if (L == NULL)
{
return false;
}
L->next = NULL;
return true;
}
// 判断单链表是否为空
bool Empty(LinkList L)
{
return L->next == NULL;
}
bool InsertNextNode(LNode *p, int e);
LNode *GetElem(LinkList L, int i);
// 在第 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++;
// }
LNode *p = GetElem(L, i - 1);
// if (p == NULL)
// {
// return false;
// }
// LNode *s = (LNode *)malloc(sizeof(LNode));
// s->data = e;
// s->next = p->next;
// p->next = s;
// return true;
return InsertNextNode(p, e);
}
// 后插操作:在结点 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;
}
// 前插操作:在结点 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;
}
// 前插操作:在结点 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;
}
// 按位序删除结点,带头结点
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++;
// }
LNode *p = GetElem(L, i - 1);
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;
}
// 按位查找:返回第 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;
}
// 按值查找,找到数据域 == e 的结点
LNode *LocateElem(LinkList L, int e)
{
LNode *p = L->next;
// 从第 1 个结点开始查找数据域为 e 的结点
while (p != NULL && p->data != e)
{
p = p->next;
}
// 找到后返回该节点指针,否则返回 NULL
return p;
}
// 求表的长度
int Length(LinkList L)
{
int len = 0;
LNode *p = L;
while (p->next != NULL)
{
p = p->next;
len++;
}
return len;
}
int main()
{
LinkList L;
InitList(L);
ListInsert(L, 1, 1);
ListInsert(L, 2, 2);
ListInsert(L, 3, 3);
ListInsert(L, 4, 4);
ListInsert(L, 5, 5);
ListInsert(L, 3, 100);
// 打印操作
LNode *p;
p = L->next;
while (p != NULL)
{
printf("%d\n", p->data);
p = p->next;
}
// 测试删除操作
int e = -1;
if (ListDelete(L, 5, e))
{
printf("已删除的元素值为=%d\n", e);
}
else
{
printf("位序不合法,删除失败\n");
}
// 打印操作
p = L->next;
while (p != NULL)
{
printf("%d\n", p->data);
p = p->next;
}
// 测试按位查找
printf("位序为4的结点为=%s\n", GetElem(L, 4));
// 测试按值查找
printf("元素值为4的结点=%s\n", LocateElem(L, 4));
return 0;
}

127
ch2/single-link/without.cpp Normal file
View File

@@ -0,0 +1,127 @@
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode
{
int data;
struct LNode *next;
} LNode, *LinkList;
// 初始化一个空的单链表,不带头结点
bool InitList(LinkList &L)
{
L = NULL;
return true;
}
// 判断单链表是否为空
bool Empty(LinkList L)
{
return L == NULL;
}
bool InsertNextNode(LNode *p, int e);
// 在第 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;
InsertNextNode(p, e);
}
// 后插操作:在结点 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;
}
// 前插操作:在结点 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;
}
// 前插操作:在结点 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;
}
int main()
{
LinkList L;
InitList(L);
ListInsert(L, 1, 1);
ListInsert(L, 2, 2);
ListInsert(L, 3, 3);
ListInsert(L, 4, 4);
ListInsert(L, 5, 5);
ListInsert(L, 3, 100);
// 打印操作
LNode *p = L;
while (p != NULL)
{
printf("%d\n", p->data);
p = p->next;
}
return 0;
}