# 单链表 ## 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; } ```