diff --git a/README.md b/README.md index 9f91036..f423692 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/ch2/README.md b/ch2/README.md index 89bd7da..405b726 100644 --- a/ch2/README.md +++ b/ch2/README.md @@ -56,7 +56,12 @@ Tips: ## 4. [顺序表](sequence/README.md) -## 5. 单链表 +- 优点:可随机存取,存储密度高。 +- 缺点:要求大片连续空间,改变容量不方便。 + +## 5. [单链表](single-link/README.md) + +- 缺点:无法逆向检索,有时候不太方便。 ## 6. 双链表 diff --git a/ch2/sequence/README.md b/ch2/sequence/README.md index f4723a0..bb02f16 100644 --- a/ch2/sequence/README.md +++ b/ch2/sequence/README.md @@ -89,7 +89,7 @@ C++ 可以使用 `new`,`delete` 关键字。 ## 3. 顺序表的特点 - **随机访问**:即可以在 $O(1)$ 时间内找到第 $i$ 个元素。 -- 存储密度高:每个节点只存储数据元素。 +- 存储密度高:每个结点只存储数据元素。 - 扩展容量不方便:静态分配不能扩展容量,动态分配可以扩展但时间复杂度高。 - 插入、删除元素操作不方便,需要移动大量元素。 diff --git a/ch2/single-link/README.md b/ch2/single-link/README.md new file mode 100644 index 0000000..be8a2e7 --- /dev/null +++ b/ch2/single-link/README.md @@ -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; +} +``` diff --git a/ch2/single-link/tail_and_head.cpp b/ch2/single-link/tail_and_head.cpp new file mode 100644 index 0000000..a135a6b --- /dev/null +++ b/ch2/single-link/tail_and_head.cpp @@ -0,0 +1,98 @@ +#include +#include +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; +} diff --git a/ch2/single-link/with.cpp b/ch2/single-link/with.cpp new file mode 100644 index 0000000..802c6be --- /dev/null +++ b/ch2/single-link/with.cpp @@ -0,0 +1,224 @@ +#include +#include +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; +} diff --git a/ch2/single-link/without.cpp b/ch2/single-link/without.cpp new file mode 100644 index 0000000..a85c3df --- /dev/null +++ b/ch2/single-link/without.cpp @@ -0,0 +1,127 @@ +#include +#include +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; +}