数据结构与算法(2)-线性表的应用
1、使用双指针找到第K个节点
具体思路:
双指针法:
先假设一个快指针一个慢指针,都指在第一个节点
假设要找倒数第三个节点,那先让快指针走三步,走完之后再让快指针和慢指针同步去走,直到快指针指向了链表末尾的NULL,此时慢指针指到了目标
核心思路:
int findNodeFS(Node *L,int k){Node *fast=L->next;Node *slow=L->next;for (int i = 0; i < k; i++){fast=fast->next;}while (fast!=NULL){fast=fast->next;slow=slow->next;}printf("倒数第%d个节点值为:%d\n",k,slow->data);
}
解题:
一,算法基本思想(思路)
在不改变链表结构的前提下,要找到倒数第k个节点,可以用两个指针法:
- 定义两个指针
fast
和slow
,都指向首元节点(不是头结点)。 - 先让
fast
向前移动k
步,这样fast
与slow
之间的距离就为k
。 - 然后两个指针同时往后移动,当
fast
到达链表末尾时,slow
所指的位置就是倒数第 k 个节点。 - 如果
fast
提前到达 NULL,说明链表长度小于 k,返回 0。
该算法只需遍历一遍链表,时间复杂度 O(n),空间复杂度 O(1)。
二,算法详细实现步骤
- 用两个指针
fast
、slow
指向首元节点; fast
向前移动k
步,如果此时fast == NULL
,说明 k 大于链表长度,返回 0;- 否则同时移动
fast
和slow
,直到fast == NULL
; - 此时
slow
即为倒数第 k 个节点; - 输出其
data
域值并返回 1。
三、C语言完整实现(含注释)
#include <stdio.h>
#include <stdlib.h>typedef int ElemType;
typedef struct node {ElemType data;struct node *next;
} Node;// 查找倒数第k个节点
int findNodeFS(Node *L, int k) {Node *fast = L->next; // 首元结点Node *slow = L->next;int i;// fast先走k步for (i = 0; i < k; i++) {if (fast == NULL) {return 0; // 链表长度小于k}fast = fast->next;}// fast和slow同时走,直到fast到达末尾while (fast != NULL) {fast = fast->next;slow = slow->next;}printf("倒数第%d个节点的值为: %d\n", k, slow->data);return 1;
}// 辅助函数:创建链表
Node* createList(int n) {Node *head = (Node*)malloc(sizeof(Node));head->next = NULL;Node *tail = head;for (int i = 1; i <= n; i++) {Node *p = (Node*)malloc(sizeof(Node));p->data = i;p->next = NULL;tail->next = p;tail = p;}return head;
}int main() {Node *L = createList(5); // 创建一个 1→2→3→4→5 的链表int k = 2;if (!findNodeFS(L, k))printf("查找失败:链表长度小于 %d\n", k);return 0;
}输出:
倒数第2个节点的值为: 4
四、总结
方法 | 时间复杂度 | 空间复杂度 | 思想 |
---|---|---|---|
双指针法 | O(n) | O(1) | 一次遍历即可找到倒数第k个节点 |
2、找相同后缀
核心思路:
- 分别求出两个链表的长度m和n
- fast指针指向较长的链表,其先走m-n或者n-m步
- 同步移动指针,判断他们是否指向同一节点.当指向同一节点时,找到了
核心思路:
typedef struct Node {char data;struct Node *next;
} Node;/* 计算带头结点单链表的长度(不含头结点) */
int length(Node *head) {int len = 0;for (Node *p = head->next; p != NULL; p = p->next) len++;return len;
}/* 从当前结点向前走 k 步(p 是首元结点而不是头结点) */
Node* advance(Node *p, int k) {while (k-- > 0 && p) p = p->next;return p;
}/* 返回两个带头结点单链表“共享后缀”的起始结点;若无则返回 NULL */
Node* find_common_tail(Node *str1, Node *str2) {Node *p1 = str1->next; // 首元结点Node *p2 = str2->next;int m = length(str1);int n = length(str2);// 让较长链表的指针先走 |m-n| 步,对齐剩余长度if (m > n) p1 = advance(p1, m - n);else p2 = advance(p2, n - m);// 同步前进,第一次指针相等处即为共享后缀起点while (p1 && p2 && p1 != p2) {p1 = p1->next;p2 = p2->next;}return (p1 == p2) ? p1 : NULL;
}
做题目:
- 基本设计思路
- 设两条带头结点的单链表分别为
str1
、str2
,首元结点为str1->next
、str2->next
。 - 先分别求出两表长度
m、n
(不含头结点)。 - 让较长表的指针先前进
|m-n|
步,使两指针到尾部的剩余长度相同(对齐)。 - 然后两指针同步前进,第一次出现 指针地址相等 的结点即为“共享后缀”的起点;若直到
NULL
都未相等,则不存在共享后缀。
关键点:判断“同一结点”必须比较指针地址(p1 == p2
),不能比较 data
。
- 代码实现(C,含必要注释)
#include <stdio.h>
#include <stdlib.h>typedef struct Node {char data;struct Node *next;
} Node;// 初始化链表(带头结点)
Node* initList() {Node *head = (Node*)malloc(sizeof(Node));head->data = 0; // 头结点数据无效head->next = NULL;return head;
}// 创建新节点
Node* newNode(char ch) {Node *p = (Node*)malloc(sizeof(Node));p->data = ch;p->next = NULL;return p;
}// 计算链表长度(不含头结点)
int length(Node *L) {int len = 0;Node *p = L->next;while (p) {len++;p = p->next;}return len;
}// 从当前结点前进k步
Node* advance(Node *p, int k) {while (k-- > 0 && p) p = p->next;return p;
}// 查找两个链表的公共后缀起点
Node* findCommon(Node *L1, Node *L2) {Node *p1 = L1->next;Node *p2 = L2->next;int len1 = length(L1);int len2 = length(L2);// 对齐:让长的先走 |len1 - len2| 步if (len1 > len2) p1 = advance(p1, len1 - len2);else p2 = advance(p2, len2 - len1);// 同步前进,直到相遇while (p1 && p2 && p1 != p2) {p1 = p1->next;p2 = p2->next;}return (p1 == p2) ? p1 : NULL;
}// 打印链表
void printList(Node *L) {Node *p = L->next;while (p) {printf("%c ", p->data);p = p->next;}printf("\n");
}int main() {// 创建两个带头结点的链表Node *str1 = initList();Node *str2 = initList();// 创建共享后缀 "i"->"n"->"g"Node *i = newNode('i');Node *n = newNode('n');Node *g = newNode('g');i->next = n;n->next = g;// 构造 str1: l->o->a->d->i->n->gNode *l = newNode('l');Node *o = newNode('o');Node *a = newNode('a');Node *d = newNode('d');str1->next = l;l->next = o;o->next = a;a->next = d;d->next = i;// 构造 str2: b->e->i->n->gNode *b = newNode('b');Node *e = newNode('e');str2->next = b;b->next = e;e->next = i;// 打印链表printf("str1: ");printList(str1);printf("str2: ");printList(str2);// 查找公共后缀起点Node *p = findCommon(str1, str2);if (p)printf("公共后缀起点字符为: %c\n", p->data);elseprintf("无公共后缀\n");return 0;
}
- 时间复杂度说明
- 计算两个长度:各遍历一次,代价
O(m) + O(n)
; - 对齐时最多前进
|m-n|
步; - 同步扫描最多再走
min(m, n)
步; - 整体仍是一次线性遍历数量级,**时间复杂度 **
O(m+n)
;只用常数个指针变量,**空间复杂度 **O(1)
。
3、去除相同元素(绝对值相同)
思路:
题目第一句话的意思是:
节点数是固定的 n 个
每个节点的数据值范围是 [-n, n] 之间
比如说 n = 5
,那这个链表中就有 5 个节点,每个节点的 data
满足:
|data| ≤ 5
即 data ∈ {-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5}
拿空间换时间,遍历一次即可
做一个数组,下标从0到21,因为链表中的值的绝对值最大是21,我们从链表第一个结点开始,链表第一个值是21,那么数组下标为21处的值设为1,链表第二个数的绝对值是15,那么数组下标为15处的绝对值设成15,链表第三个数的绝对值是15,此时去数组发现下标为15的地方已经标值1了,所以这个是重复的,把链表第三个节点删除.循此往后直到链表末尾即可
核心思路:
void removeNode(Node *L, int n)
{Node *p = L; // p 指向“待检查结点”的前驱(从头结点开始)int index;int *q = (int*)malloc(sizeof(int) * (n + 1)); // 标记数组 seen[0..n]// 初始化标记数组为 0for (int i = 0; i < n + 1; i++) *(q + i) = 0;while (p->next != NULL) {// 取被检查结点的绝对值index = abs(p->next->data);if (*(q + index) == 0) { // 第一次出现:做标记,后移 p*(q + index) = 1;p = p->next;} else { // 重复出现:删除 p->nextNode *temp = p->next;p->next = temp->next;free(temp);}}free(q);
}
- 基本设计思路
- 题设给出:链表存 n 个整数,且对任一结点
|data| ≤ n
。 - 开一个标记数组
seen[0..n]
,初值全 0。 - 设指针
p
从头结点开始,始终保持p
是“待检查结点的前驱”。- 取
index = abs(p->next->data)
- 若
seen[index] == 0
:说明该绝对值第一次出现,置seen[index]=1
,p = p->next
- 否则:该绝对值重复,删除
p->next
(p->next = p->next->next; free(被删结点)
),p
不动
- 取
- 一趟扫描完成。
- 结点数据类型定义
typedef struct Node {int data; // 结点的整数值,可能为负,且 |data| ≤ nstruct Node *next; // 指向后继
} Node;
- C 代码
#include <stdio.h>
#include <stdlib.h> // malloc, free
#include <math.h> // abstypedef struct Node {int data;struct Node *next;
} Node;/* 带头结点初始化 */
Node* initList(void){Node *head = (Node*)malloc(sizeof(Node));head->data = 0;head->next = NULL;return head;
}/* 新结点 */
Node* newNode(int x){Node *p = (Node*)malloc(sizeof(Node));p->data = x;p->next = NULL;return p;
}/* 尾插(演示用) */
void push_back(Node *L, int x){Node *p = L;while (p->next) p = p->next;p->next = newNode(x);
}/* 打印(不含头结点) */
void printList(Node *L){for (Node *p = L->next; p; p = p->next) printf("%d ", p->data);printf("\n");
}/* ---------- 关键:删除绝对值相同(仅保留首次出现) ---------- */
/* 参数 n 是 |data| 的上界(题设给定或可由输入得知) */
void removeNode(Node *L, int n){Node *p = L; // p 为前驱,从头结点开始int *seen = (int*)malloc(sizeof(int) * (n + 1));if (!seen) return;// 初始化标记数组for (int i = 0; i <= n; ++i) seen[i] = 0;while (p->next != NULL){int index = abs(p->next->data); // 取绝对值if (seen[index] == 0){ // 首次出现,做标记并前进seen[index] = 1;p = p->next;}else{ // 重复 -> 删除 p->nextNode *temp = p->next;p->next = temp->next;free(temp);}}free(seen);
}int main(){// head: 21 -> -15 -> -15 -> -7 -> 15Node *head = initList();push_back(head, 21);push_back(head, -15);push_back(head, -15);push_back(head, -7);push_back(head, 15);printf("原链表: ");printList(head);removeNode(head, 21); // n 取绝对值上界(本例为 21)printf("删除后: ");printList(head); // 期望输出:21 -15 -7return 0;
}
- 复杂度
- 时间复杂度:一次线性扫描,每个结点 O(1) 处理 ⇒ O(n)(这里 n 指结点个数)。
- 空间复杂度:标记数组
seen[0..n]
(这里的 n 为数据上界) ⇒ O(n)。
4、反转链表
思路
- first指向空值,second指向1,third指向second的下一个节点
- 用second指向空值,然后挪,然后再让second指回1,再挪
- 直到second指向NULL,再加个头节点
```c
Node* reverseList(Node *head) {Node *first = NULL; // 已反转部分的头Node *second = head->next; // 待反转部分Node *third; // 暂存下一节点while (second != NULL) {third = second->next; // 暂存 nextsecond->next = first; // 当前节点指向已反转部分first = second; // first 前进second = third; // second 前进}// 建立新的头结点Node *hd = initList();hd->next = first;return hd;
}
#include <stdio.h>
#include <stdlib.h>typedef struct Node {int data;struct Node *next;
} Node;// 初始化带头结点的链表
Node* initList(void) {Node *head = (Node*)malloc(sizeof(Node));head->data = 0;head->next = NULL;return head;
}// 创建新节点
Node* newNode(int x) {Node *p = (Node*)malloc(sizeof(Node));p->data = x;p->next = NULL;return p;
}// 尾插
void push_back(Node *L, int x) {Node *p = L;while (p->next) p = p->next;p->next = newNode(x);
}// 打印链表
void printList(Node *L) {Node *p = L->next;while (p) {printf("%d ", p->data);p = p->next;}printf("\n");
}/* ---------------- 链表反转核心函数 ---------------- */
Node* reverseList(Node *head) {Node *first = NULL; // 已反转部分的头Node *second = head->next; // 待反转部分Node *third; // 暂存下一节点while (second != NULL) {third = second->next; // 暂存 nextsecond->next = first; // 当前节点指向已反转部分first = second; // first 前进second = third; // second 前进}// 建立新的头结点Node *hd = initList();hd->next = first;return hd;
}/* ---------------- 测试程序 ---------------- */
int main() {Node *head = initList();push_back(head, 1);push_back(head, 2);push_back(head, 3);push_back(head, 4);push_back(head, 5);printf("原链表: ");printList(head);Node *rev = reverseList(head);printf("反转后: ");printList(rev);return 0;
}
5、删除链表中间节点
- fast指向1,slow指向head
- fast走两步(fast=fast->next->next),slow走一步(slow=slow->next)
- 发现fast是NULL或者fast的下一个节点是NULL的时候,
int delMiddleNode(Node *head){Node *fast=head->next;Node *slow=head;while (fast!=NULL && fast->next!=NULL){fast=fast->next->next;slow=slow->next;}// 定义指针变量指向要删除的那个节点Node *q=slow->next;slow->next=q->next;free(q);return 1;
}
完整代码:
#include <stdio.h>
#include <stdlib.h>typedef struct Node {int data;struct Node *next;
} Node;// 初始化链表
Node* initList(void) {Node *head = (Node*)malloc(sizeof(Node));head->next = NULL;return head;
}// 尾插
void push_back(Node *L, int x) {Node *p = L;while (p->next) p = p->next;Node *q = (Node*)malloc(sizeof(Node));q->data = x;q->next = NULL;p->next = q;
}// 打印
void printList(Node *L) {Node *p = L->next;while (p) {printf("%d ", p->data);p = p->next;}printf("\n");
}// 删除中间节点
int delMiddleNode(Node *head) {if (head == NULL || head->next == NULL) return 0; // 空表Node *fast = head->next;Node *slow = head;while (fast != NULL && fast->next != NULL) {fast = fast->next->next;slow = slow->next;}Node *q = slow->next;slow->next = q->next;free(q);return 1;
}int main() {Node *head = initList();for (int i = 1; i <= 5; i++)push_back(head, i);printf("原链表: ");printList(head);delMiddleNode(head);printf("删除中间节点后: ");printList(head);return 0;
}
6、做个题目练练
效果如图所示:
- 找到中间的位置,断开
- 把后半截反转
- 6插到1和2中间,5插到2和3中间,4放到3后面.
利用四个指针 p1
、p2
、q1
、q2
实现两条链表的交叉合并。p1
指向第一条链表当前节点, p2
指向 p1
的下一个节点; q1
指向第二条链表当前节点, q2
指向 q1
的下一个节点。每次操作时,先将 q1
插入到 p1
和 p2
之间(即 p1->next=q1
, q1->next=p2
),使第二条链表当前节点嵌入第一条链表对应位置;然后四个指针整体后移(p1=p2
, q1=q2
),继续进行下一轮插入。如此循环,直到任意一条链表遍历完为止,就能让两条链表的节点像拉链一样交错连接成一个完整的新链表
也就是
初始状态
A链表: Head → 1 → 2 → 3 → NULL
B链表: 6 → 5 → 4 → NULL 指针:
p1 → 1
p2 → 2
q1 → 6
q2 → 5
下一步:把6插到1和2中间
操作:
p1->next = q1
q1->next = p2结果:
Head → 1 → 6 → 2 → 3
B剩余:5 → 4更新指针:
p1 → 2
p2 → 3
q1 → 5
q2 → 4
下一步:把5插到2和3之间
操作:
p1->next = q1
q1->next = p2结果:
Head → 1 → 6 → 2 → 5 → 3
B剩余:4更新指针:
p1 → 3
p2 → NULL
q1 → 4
q2 → NULL
下一步: 把4插到3后面
操作:
p1->next = q1
q1->next = p2 (此时p2为NULL)结果:
Head → 1 → 6 → 2 → 5 → 3 → 4 → NULL更新指针:
p1 → NULL
q1 → NULL
循环结束
代码实现:
void reorderList(Node *head) {if (head == NULL || head->next == NULL) return;// ① 快慢指针找中点Node *fast = head->next;Node *slow = head;while (fast != NULL && fast->next != NULL) {fast = fast->next->next;slow = slow->next;}// ② 反转后半链表Node *first = NULL;Node *second = slow->next;slow->next = NULL; // 截断前半部分Node *third = NULL;while (second != NULL) {third = second->next;second->next = first;first = second;second = third;}// ③ 交叉合并前后两半Node *p1 = head->next;Node *q1 = first;Node *p2, *q2;while (p1 != NULL && q1 != NULL) {p2 = p1->next;q2 = q1->next;p1->next = q1;q1->next = p2;p1 = p2;q1 = q2;}
}
整道题答案:
#include <stdio.h>
#include <stdlib.h>typedef struct Node {int data;struct Node *next;
} Node;Node* initList(void) {Node *head = (Node*)malloc(sizeof(Node));head->next = NULL;return head;
}void push_back(Node *L, int x) {Node *p = L;while (p->next) p = p->next;Node *q = (Node*)malloc(sizeof(Node));q->data = x;q->next = NULL;p->next = q;
}void printList(Node *L) {Node *p = L->next;while (p) {printf("%d ", p->data);p = p->next;}printf("\n");
}/* -------------------- 重新排列链表 -------------------- */
/*
目标:1→2→3→4→5 → 1→5→2→4→3
步骤:1. 快慢指针找到中点2. 反转后半部分链表3. 合并两部分链表
*/
void reorderList(Node *head) {if (head == NULL || head->next == NULL) return;// ① 快慢指针找中点Node *fast = head->next;Node *slow = head;while (fast != NULL && fast->next != NULL) {fast = fast->next->next;slow = slow->next;}// ② 反转后半链表Node *first = NULL;Node *second = slow->next;slow->next = NULL; // 截断前半部分Node *third = NULL;while (second != NULL) {third = second->next;second->next = first;first = second;second = third;}// ③ 交叉合并前后两半Node *p1 = head->next;Node *q1 = first;Node *p2, *q2;while (p1 != NULL && q1 != NULL) {p2 = p1->next;q2 = q1->next;p1->next = q1;q1->next = p2;p1 = p2;q1 = q2;}
}/* -------------------- 测试 -------------------- */
int main() {Node *head = initList();for (int i = 1; i <= 5; i++) push_back(head, i);printf("原链表: ");printList(head);reorderList(head);printf("重新排列后: ");printList(head);return 0;
}
7、单链表的局限:
不可以回头
于是想个办法:
- 单向循环链表
循环链表(Circular Linked List)是另一种形式的链式存储结构。其特点是表中最后一个节点的指针域指向头节点,整个链表形成一个环
当链表遍历时,判别当前指针 p 是否指向表尾结点的终止条件不同。在单链表中,判别条件为 p != NULL 或 p->next != NULL,而循环链表的判别条件为 p != L 或 p->next != L
通常这么玩:
fast和slow先都指向head,fast走两步,slow走一步,如果两个指针可以相遇,说明有环
int isCycle(Node *head){Node *fast=head;Node *slow=head;while (fast!=NULL && fast->next!=NULL){fast=fast->next->next;slow=slow->next;if (fast==slow){return 1;}}return 0;
}