
个人主页:
wengqidaifeng
✨ 永远在路上,永远向前走
文章目录
- 前言
- 一、轮转数组
- 二、消失的数字
- 三、返回倒数第k个节点
- 四、链表的回文结构
-
- 题目理解
-
- 方法一:复制到数组(不符合空间要求,但逻辑简单)
- [方法二:快慢指针 + 反转链表(最优解)](#方法二:快慢指针 + 反转链表(最优解))
- 方法三:递归法
- 测试代码
- 五、相交链表
- 六、随机链表的复制
- 总结与归纳
前言
本文使用C语言重新实现六大经典算法题,C语言的指针操作能够更深入地理解数据结构的底层原理。每道题都提供多种解法,并附有详细的注释,帮助读者掌握C语言操作数组和链表的技巧。
一、轮转数组
题目理解
将数组元素向右移动k位,相当于把末尾的k个元素移到开头。需要注意k可能大于数组长度,需要先取模。
方法一:使用辅助数组(最直观)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/**
* 轮转数组 - 辅助数组法
* @param nums: 原数组
* @param numsSize: 数组大小
* @param k: 轮转步数
*
* 思路:新数组的第(i+k)%n个位置存放原数组的第i个元素
* 时间复杂度:O(n)
* 空间复杂度:O(n)
*/
void rotate_extraArray(int* nums, int numsSize, int k) {
// 处理k大于数组长度的情况
k = k % numsSize;
if (k == 0) return; // 不需要轮转
// 动态分配辅助数组
int* extra = (int*)malloc(sizeof(int) * numsSize);
if (extra == NULL) return; // 内存分配失败处理
// 将元素放到正确位置
for (int i = 0; i < numsSize; i++) {
// (i + k) % numsSize 是原数组第i个元素应该去的位置
extra[(i + k) % numsSize] = nums[i];
}
// 将辅助数组拷贝回原数组
// 使用memcpy效率更高,但要确保不会内存重叠
memcpy(nums, extra, sizeof(int) * numsSize);
// 释放辅助数组内存
free(extra);
}
方法二:三次反转(原地算法)
c
/**
* 反转数组中指定区间的元素
* @param nums: 数组
* @param start: 起始下标
* @param end: 结束下标
*
* 思路:双指针从两端向中间交换
*/
void reverse(int* nums, int start, int end) {
while (start < end) {
// 交换两个元素
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
// 移动指针
start++;
end--;
}
}
/**
* 轮转数组 - 三次反转法
* @param nums: 原数组
* @param numsSize: 数组大小
* @param k: 轮转步数
*
* 思路:
* 1. 反转整个数组
* 2. 反转前k个元素
* 3. 反转剩余元素
*
* 例如:[1,2,3,4,5,6,7], k=3
* 第一次反转:[7,6,5,4,3,2,1]
* 第二次反转:[5,6,7,4,3,2,1]
* 第三次反转:[5,6,7,1,2,3,4]
*
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
void rotate_reverse(int* nums, int numsSize, int k) {
// 处理k大于数组长度的情况
k = k % numsSize;
if (k == 0) return;
// 1. 反转整个数组
reverse(nums, 0, numsSize - 1);
// 2. 反转前k个元素
reverse(nums, 0, k - 1);
// 3. 反转剩余元素
reverse(nums, k, numsSize - 1);
}
方法三:环状替换(数学方法)
c
/**
* 轮转数组 - 环状替换法
* @param nums: 原数组
* @param numsSize: 数组大小
* @param k: 轮转步数
*
* 思路:将数组视为一个环,每个元素移动到正确位置后,
* 会替换出另一个元素,继续移动被替换的元素
*
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
void rotate_cycle(int* nums, int numsSize, int k) {
k = k % numsSize;
if (k == 0) return;
int count = 0; // 记录已经移动的元素个数
// 从每个可能的起始位置开始循环
// 为什么需要多个起始位置?因为可能存在多个独立的环
for (int start = 0; count < numsSize; start++) {
int current = start; // 当前要移动的位置
int prev = nums[start]; // 保存当前位置的值
do {
// 计算下一个位置
int next = (current + k) % numsSize;
// 保存下一个位置的值,并将prev放进去
int temp = nums[next];
nums[next] = prev;
prev = temp;
// 移动到下一个位置
current = next;
count++;
// 当回到起点时,一个环结束
} while (start != current);
}
}
测试代码
c
#include <stdio.h>
// 打印数组函数
void printArray(int* nums, int size) {
printf("[");
for (int i = 0; i < size; i++) {
printf("%d", nums[i]);
if (i < size - 1) printf(", ");
}
printf("]\n");
}
// 测试轮转数组
void testRotate() {
printf("\n=== 测试轮转数组 ===\n");
int nums1[] = {1, 2, 3, 4, 5, 6, 7};
int size1 = sizeof(nums1) / sizeof(nums1[0]);
int k1 = 3;
printf("原数组: ");
printArray(nums1, size1);
printf("向右轮转 %d 步\n", k1);
// 复制数组进行不同方法的测试
int nums1_copy[size1];
memcpy(nums1_copy, nums1, sizeof(nums1));
rotate_reverse(nums1_copy, size1, k1);
printf("三次反转法结果: ");
printArray(nums1_copy, size1);
int nums2[] = {-1, -100, 3, 99};
int size2 = sizeof(nums2) / sizeof(nums2[0]);
int k2 = 2;
printf("\n原数组: ");
printArray(nums2, size2);
printf("向右轮转 %d 步\n", k2);
rotate_cycle(nums2, size2, k2);
printf("环状替换法结果: ");
printArray(nums2, size2);
}
二、消失的数字
题目理解
数组包含0~n的所有整数但缺一个,找出缺失的。n = 数组长度,因为缺一个所以最大数是n。
方法一:数学求和
c
/**
* 消失的数字 - 数学求和法
* @param nums: 原数组
* @param numsSize: 数组大小
* @return: 缺失的数字
*
* 思路:0~n的和减去数组中所有数的和
* 公式:n*(n+1)/2 - sum
*
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
int missingNumber_sum(int* nums, int numsSize) {
// 计算0~n的和,n = numsSize
// 注意:因为缺失一个数,所以最大数是numsSize
long expectedSum = (long)numsSize * (numsSize + 1) / 2;
// 计算数组中所有数的和
long actualSum = 0;
for (int i = 0; i < numsSize; i++) {
actualSum += nums[i];
}
// 差值就是缺失的数
return (int)(expectedSum - actualSum);
}
方法二:异或运算(最优解)
c
/**
* 消失的数字 - 异或法
* @param nums: 原数组
* @param numsSize: 数组大小
* @return: 缺失的数字
*
* 思路:利用异或的性质:a^a=0, a^0=a
* 将0~n与数组中的所有数进行异或,成对出现的数会抵消
* 最终剩下的就是缺失的数
*
* 例如:[3,0,1] n=3
* 异或:0^1^2^3 ^ 3^0^1 = 2
*
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
int missingNumber_xor(int* nums, int numsSize) {
int xor = 0;
// 先与0~n异或
for (int i = 0; i <= numsSize; i++) {
xor ^= i;
}
// 再与数组中的数异或
for (int i = 0; i < numsSize; i++) {
xor ^= nums[i];
}
return xor;
}
/**
* 异或法的另一种写法(一次循环)
*/
int missingNumber_xor_optimized(int* nums, int numsSize) {
int xor = numsSize; // 先异或n
for (int i = 0; i < numsSize; i++) {
xor ^= i ^ nums[i]; // 同时异或下标和元素值
}
return xor;
}
方法三:哈希数组
c
/**
* 消失的数字 - 哈希法
* @param nums: 原数组
* @param numsSize: 数组大小
* @return: 缺失的数字
*
* 思路:使用额外的数组标记出现的数字
*
* 时间复杂度:O(n)
* 空间复杂度:O(n)
*/
int missingNumber_hash(int* nums, int numsSize) {
// 动态分配哈希表,初始化为0
// calloc会自动初始化为0
int* hash = (int*)calloc(numsSize + 1, sizeof(int));
if (hash == NULL) return -1; // 内存分配失败
// 标记出现的数字
for (int i = 0; i < numsSize; i++) {
hash[nums[i]] = 1;
}
// 查找未出现的数字
int result = -1;
for (int i = 0; i <= numsSize; i++) {
if (hash[i] == 0) {
result = i;
break;
}
}
free(hash); // 释放内存
return result;
}
测试代码
c
// 测试消失的数字
void testMissingNumber() {
printf("\n=== 测试消失的数字 ===\n");
int nums1[] = {3, 0, 1};
int size1 = sizeof(nums1) / sizeof(nums1[0]);
printf("数组: [3,0,1]\n");
printf("数学求和法: 缺失 %d\n", missingNumber_sum(nums1, size1));
printf("异或法: 缺失 %d\n", missingNumber_xor(nums1, size1));
int nums2[] = {9,6,4,2,3,5,7,0,1};
int size2 = sizeof(nums2) / sizeof(nums2[0]);
printf("\n数组: [9,6,4,2,3,5,7,0,1]\n");
printf("数学求和法: 缺失 %d\n", missingNumber_sum(nums2, size2));
printf("异或法: 缺失 %d\n", missingNumber_xor(nums2, size2));
}
三、返回倒数第k个节点
链表节点定义
c
#include <stdio.h>
#include <stdlib.h>
// 单链表节点定义
struct ListNode {
int val;
struct ListNode *next;
};
// 为了方便,定义类型别名
typedef struct ListNode ListNode;
方法一:两次遍历
c
/**
* 返回倒数第k个节点 - 两次遍历法
* @param head: 链表头指针
* @param k: 倒数第k个
* @return: 节点的值
*
* 思路:先遍历一次得到链表长度,再遍历到第n-k个节点
*
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
int kthToLast_twice(ListNode* head, int k) {
if (head == NULL) return -1; // 空链表
// 第一次遍历:计算链表长度
ListNode* cur = head;
int length = 0;
while (cur != NULL) {
length++;
cur = cur->next;
}
// 第二次遍历:走到第length-k个节点
cur = head;
for (int i = 0; i < length - k; i++) {
cur = cur->next;
}
return cur->val;
}
方法二:快慢指针(最优解)
c
/**
* 返回倒数第k个节点 - 快慢指针法
* @param head: 链表头指针
* @param k: 倒数第k个
* @return: 节点的值
*
* 思路:
* 1. 快指针先走k步
* 2. 然后快慢指针同时走
* 3. 当快指针到达末尾时,慢指针正好在倒数第k个
*
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
int kthToLast_fastSlow(ListNode* head, int k) {
if (head == NULL) return -1;
ListNode* fast = head;
ListNode* slow = head;
// 1. 快指针先走k步
for (int i = 0; i < k; i++) {
if (fast == NULL) {
return -1; // k大于链表长度
}
fast = fast->next;
}
// 2. 同时移动,直到快指针到达末尾
while (fast != NULL) {
fast = fast->next;
slow = slow->next;
}
return slow->val;
}
方法三:递归法
c
// 全局变量或静态变量用于递归计数
// 注意:多线程环境下不安全,这里仅为演示
static int count;
/**
* 递归辅助函数
*/
ListNode* kthToLast_recursiveHelper(ListNode* head, int k) {
if (head == NULL) {
count = 0;
return NULL;
}
// 递归到链表末尾
ListNode* node = kthToLast_recursiveHelper(head->next, k);
count++;
// 当count等于k时,当前节点就是倒数第k个
if (count == k) {
return head;
}
return node;
}
/**
* 返回倒数第k个节点 - 递归法
* @param head: 链表头指针
* @param k: 倒数第k个
* @return: 节点的值
*
* 思路:利用递归的回溯过程计数
*
* 时间复杂度:O(n)
* 空间复杂度:O(n) 递归栈
*/
int kthToLast_recursive(ListNode* head, int k) {
ListNode* result = kthToLast_recursiveHelper(head, k);
return result ? result->val : -1;
}
辅助函数:创建链表
c
/**
* 创建链表节点
*/
ListNode* createNode(int val) {
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
if (node != NULL) {
node->val = val;
node->next = NULL;
}
return node;
}
/**
* 根据数组创建链表
*/
ListNode* createList(int* arr, int size) {
if (size == 0) return NULL;
ListNode* head = createNode(arr[0]);
ListNode* cur = head;
for (int i = 1; i < size; i++) {
cur->next = createNode(arr[i]);
cur = cur->next;
}
return head;
}
/**
* 打印链表
*/
void printList(ListNode* head) {
printf("链表: ");
while (head != NULL) {
printf("%d", head->val);
if (head->next != NULL) {
printf(" -> ");
}
head = head->next;
}
printf("\n");
}
/**
* 释放链表内存
*/
void freeList(ListNode* head) {
while (head != NULL) {
ListNode* temp = head;
head = head->next;
free(temp);
}
}
测试代码
c
// 测试返回倒数第k个节点
void testKthToLast() {
printf("\n=== 测试返回倒数第k个节点 ===\n");
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
int k = 2;
ListNode* head = createList(arr, size);
printList(head);
printf("倒数第 %d 个节点: %d\n", k, kthToLast_fastSlow(head, k));
printf("倒数第 %d 个节点(两次遍历): %d\n", k, kthToLast_twice(head, k));
freeList(head);
}
四、链表的回文结构
题目理解
判断链表是否回文:正序和逆序遍历值相同。要求时间复杂度O(n),空间复杂度O(1)。
方法一:复制到数组(不符合空间要求,但逻辑简单)
c
/**
* 判断回文链表 - 数组法
* @param head: 链表头指针
* @return: 1表示回文,0表示非回文
*
* 思路:将链表值复制到数组,然后用双指针判断
*
* 时间复杂度:O(n)
* 空间复杂度:O(n)
*/
int isPalindrome_array(ListNode* head) {
if (head == NULL || head->next == NULL) {
return 1; // 空链表或单节点链表视为回文
}
// 第一次遍历,获取链表长度
int length = 0;
ListNode* cur = head;
while (cur != NULL) {
length++;
cur = cur->next;
}
// 动态分配数组
int* arr = (int*)malloc(sizeof(int) * length);
if (arr == NULL) return 0;
// 第二次遍历,复制值到数组
cur = head;
for (int i = 0; i < length; i++) {
arr[i] = cur->val;
cur = cur->next;
}
// 双指针判断回文
int left = 0, right = length - 1;
int result = 1;
while (left < right) {
if (arr[left] != arr[right]) {
result = 0;
break;
}
left++;
right--;
}
free(arr);
return result;
}
方法二:快慢指针 + 反转链表(最优解)
c
/**
* 反转链表
* @param head: 待反转链表的头指针
* @return: 反转后的链表头指针
*
* 思路:使用三个指针prev, curr, next进行迭代反转
*/
ListNode* reverseList(ListNode* head) {
ListNode* prev = NULL;
ListNode* curr = head;
while (curr != NULL) {
ListNode* next = curr->next; // 保存下一个节点
curr->next = prev; // 反转指针
prev = curr; // 移动prev
curr = next; // 移动curr
}
return prev; // prev是新的头节点
}
/**
* 判断回文链表 - 快慢指针+反转法
* @param head: 链表头指针
* @return: 1表示回文,0表示非回文
*
* 思路:
* 1. 找到链表中点(快慢指针)
* 2. 反转后半部分链表
* 3. 比较前半部分和反转后的后半部分
* 4. 恢复链表(可选)
*
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
int isPalindrome_optimal(ListNode* head) {
if (head == NULL || head->next == NULL) {
return 1;
}
// 1. 找到链表中点
ListNode* slow = head;
ListNode* fast = head;
// 快指针每次走两步,慢指针每次走一步
// 当快指针到达末尾时,慢指针在中点
while (fast->next != NULL && fast->next->next != NULL) {
slow = slow->next;
fast = fast->next->next;
}
// 2. 反转后半部分链表
ListNode* secondHalf = reverseList(slow->next);
// 3. 比较前半部分和后半部分
ListNode* p1 = head;
ListNode* p2 = secondHalf;
int result = 1;
while (p2 != NULL) { // 只需比较后半部分长度
if (p1->val != p2->val) {
result = 0;
break;
}
p1 = p1->next;
p2 = p2->next;
}
// 4. 恢复链表(可选,但为了不破坏原链表结构)
slow->next = reverseList(secondHalf);
return result;
}
方法三:递归法
c
// 全局变量,用于递归比较
static ListNode* left;
/**
* 递归比较函数
*/
int recursivelyCheck(ListNode* right) {
if (right == NULL) return 1;
// 先递归到链表末尾
if (!recursivelyCheck(right->next)) {
return 0;
}
// 比较左指针和右指针的值
if (left->val != right->val) {
return 0;
}
// 左指针右移
left = left->next;
return 1;
}
/**
* 判断回文链表 - 递归法
* @param head: 链表头指针
* @return: 1表示回文,0表示非回文
*
* 思路:利用递归的回溯过程从后往前比较
*
* 时间复杂度:O(n)
* 空间复杂度:O(n) 递归栈
*/
int isPalindrome_recursive(ListNode* head) {
left = head;
return recursivelyCheck(head);
}
测试代码
c
// 测试回文链表
void testPalindrome() {
printf("\n=== 测试链表的回文结构 ===\n");
// 测试用例1:回文链表
int arr1[] = {1, 2, 3, 2, 1};
ListNode* head1 = createList(arr1, sizeof(arr1)/sizeof(arr1[0]));
printList(head1);
printf("是否为回文: %s\n", isPalindrome_optimal(head1) ? "是" : "否");
// 测试用例2:非回文链表
int arr2[] = {1, 2, 3, 4, 5};
ListNode* head2 = createList(arr2, sizeof(arr2)/sizeof(arr2[0]));
printList(head2);
printf("是否为回文: %s\n", isPalindrome_optimal(head2) ? "是" : "否");
// 测试用例3:偶数长度回文
int arr3[] = {1, 2, 2, 1};
ListNode* head3 = createList(arr3, sizeof(arr3)/sizeof(arr3[0]));
printList(head3);
printf("是否为回文: %s\n", isPalindrome_optimal(head3) ? "是" : "否");
freeList(head1);
freeList(head2);
freeList(head3);
}
五、相交链表
题目理解
找到两个链表相交的起始节点。注意是节点相交(内存地址相同),不是值相同。
方法一:哈希集合
c
/**
* 相交链表 - 哈希法
* @param headA: 链表A的头指针
* @param headB: 链表B的头指针
* @return: 相交节点的指针,不相交返回NULL
*
* 思路:将链表A的所有节点存入哈希表,然后遍历链表B查找
*
* 时间复杂度:O(m + n)
* 空间复杂度:O(m) 或 O(n)
*
* 注意:C语言没有内置哈希表,这里用数组模拟
* 但实际地址不能作为数组索引,所以这种方法在C中不实用
* 这里仅作为思路演示
*/
ListNode* getIntersectionNode_hash(ListNode* headA, ListNode* headB) {
// C语言中很难实现真正的哈希表(因为地址范围太大)
// 实际可以用uthash库,这里简化处理
// 真正的做法是使用集合,但C标准库没有
// 这里用双重循环代替,但时间复杂度O(m*n)
ListNode* curA = headA;
while (curA != NULL) {
ListNode* curB = headB;
while (curB != NULL) {
if (curA == curB) {
return curA;
}
curB = curB->next;
}
curA = curA->next;
}
return NULL;
}
方法二:双指针法(浪漫相遇法)
c
/**
* 相交链表 - 双指针法
* @param headA: 链表A的头指针
* @param headB: 链表B的头指针
* @return: 相交节点的指针,不相交返回NULL
*
* 思路:
* 1. 指针pA从headA开始遍历,pB从headB开始遍历
* 2. 当pA到达末尾时,转到headB继续
* 3. 当pB到达末尾时,转到headA继续
* 4. 最终它们会在相交点相遇(如果有的话)
*
* 为什么可行?
* 设A长度为a,B长度为b,相交部分长度为c
* pA走过的路程:a + (b-c)
* pB走过的路程:b + (a-c)
* 两者相等,所以会在相交点相遇
*
* 时间复杂度:O(m + n)
* 空间复杂度:O(1)
*/
ListNode* getIntersectionNode_twoPointer(ListNode* headA, ListNode* headB) {
if (headA == NULL || headB == NULL) {
return NULL;
}
ListNode* pA = headA;
ListNode* pB = headB;
// 当pA和pB相等时,要么是相交点,要么都是NULL
while (pA != pB) {
// pA移动到下一个节点,如果到达末尾则转到headB
pA = (pA == NULL) ? headB : pA->next;
// pB移动到下一个节点,如果到达末尾则转到headA
pB = (pB == NULL) ? headA : pB->next;
}
return pA; // 如果相交返回相交点,如果不相交返回NULL
}
方法三:长度差法
c
/**
* 计算链表长度
*/
int getListLength(ListNode* head) {
int length = 0;
while (head != NULL) {
length++;
head = head->next;
}
return length;
}
/**
* 相交链表 - 长度差法
* @param headA: 链表A的头指针
* @param headB: 链表B的头指针
* @return: 相交节点的指针,不相交返回NULL
*
* 思路:
* 1. 计算两个链表的长度
* 2. 让长的链表先走长度差步
* 3. 然后两个链表同时走,相遇点就是相交点
*
* 时间复杂度:O(m + n)
* 空间复杂度:O(1)
*/
ListNode* getIntersectionNode_length(ListNode* headA, ListNode* headB) {
if (headA == NULL || headB == NULL) {
return NULL;
}
// 计算两个链表的长度
int lenA = getListLength(headA);
int lenB = getListLength(headB);
// 让长的链表先走
while (lenA > lenB) {
headA = headA->next;
lenA--;
}
while (lenB > lenA) {
headB = headB->next;
lenB--;
}
// 同时走,直到相遇
while (headA != headB) {
headA = headA->next;
headB = headB->next;
}
return headA; // 可能为NULL,也可能为相交点
}
辅助函数:创建相交链表
c
/**
* 创建相交链表用于测试
* @param arrA: 链表A的公共部分之前的数组
* @param sizeA: 数组A大小
* @param arrCommon: 公共部分数组
* @param sizeCommon: 公共部分大小
* @param arrB: 链表B的公共部分之前的数组
* @param sizeB: 数组B大小
* @param headA: 输出参数,链表A的头指针
* @param headB: 输出参数,链表B的头指针
*
* 返回相交节点
*/
ListNode* createIntersectionList(int* arrA, int sizeA,
int* arrCommon, int sizeCommon,
int* arrB, int sizeB,
ListNode** headA, ListNode** headB) {
// 创建公共部分
ListNode* commonHead = createList(arrCommon, sizeCommon);
// 创建链表A的前半部分
*headA = createList(arrA, sizeA);
// 连接到公共部分
if (*headA == NULL) {
*headA = commonHead;
} else {
ListNode* cur = *headA;
while (cur->next != NULL) {
cur = cur->next;
}
cur->next = commonHead;
}
// 创建链表B的前半部分
*headB = createList(arrB, sizeB);
// 连接到公共部分
if (*headB == NULL) {
*headB = commonHead;
} else {
ListNode* cur = *headB;
while (cur->next != NULL) {
cur = cur->next;
}
cur->next = commonHead;
}
return commonHead;
}
测试代码
c
// 测试相交链表
void testIntersection() {
printf("\n=== 测试相交链表 ===\n");
// 创建相交链表:A: 4->1->8->4->5, B: 5->6->1->8->4->5
int arrA[] = {4, 1};
int arrCommon[] = {8, 4, 5};
int arrB[] = {5, 6, 1};
ListNode* headA;
ListNode* headB;
ListNode* intersection = createIntersectionList(arrA, 2, arrCommon, 3, arrB, 3, &headA, &headB);
printf("链表A: ");
printList(headA);
printf("链表B: ");
printList(headB);
ListNode* result = getIntersectionNode_twoPointer(headA, headB);
if (result != NULL) {
printf("相交节点的值: %d\n", result->val);
} else {
printf("不相交\n");
}
// 注意:这里不能简单freeList,因为公共部分被两个链表共享
// 需要特殊处理,这里省略
}
六、随机链表的复制
节点定义
c
#include <stdio.h>
#include <stdlib.h>
// 随机链表节点定义
struct RandomListNode {
int val;
struct RandomListNode *next;
struct RandomListNode *random;
};
typedef struct RandomListNode RandomListNode;
方法一:哈希表法
c
/**
* 随机链表的复制 - 哈希表法
* @param head: 原链表头指针
* @return: 复制链表的头指针
*
* 思路:
* 1. 第一次遍历:创建所有新节点,建立原节点到新节点的映射
* 2. 第二次遍历:根据映射设置next和random指针
*
* 时间复杂度:O(n)
* 空间复杂度:O(n)
*
* 注意:C语言没有内置哈希表,这里用数组模拟
* 但实际节点地址范围大,不适合用数组
* 这里为了演示,假设节点值不重复,用值作为键(实际不严谨)
*/
RandomListNode* copyRandomList_hash(RandomListNode* head) {
if (head == NULL) return NULL;
// 假设最多1000个节点
#define MAX_NODES 1000
// 存储原节点到新节点的映射(用数组模拟)
RandomListNode* map[MAX_NODES];
int index = 0;
// 第一遍遍历:创建所有新节点
RandomListNode* cur = head;
RandomListNode* newHead = NULL;
RandomListNode* newCur = NULL;
while (cur != NULL) {
RandomListNode* newNode = (RandomListNode*)malloc(sizeof(RandomListNode));
newNode->val = cur->val;
newNode->next = NULL;
newNode->random = NULL;
// 记录映射
map[index++] = newNode;
if (newHead == NULL) {
newHead = newNode;
newCur = newNode;
} else {
newCur->next = newNode;
newCur = newNode;
}
cur = cur->next;
}
// 第二遍遍历:设置random指针
cur = head;
newCur = newHead;
index = 0;
while (cur != NULL) {
if (cur->random != NULL) {
// 需要找到random指向的节点在映射中的位置
// 这里简化处理:遍历原链表找位置(效率低)
RandomListNode* temp = head;
int pos = 0;
while (temp != cur->random) {
temp = temp->next;
pos++;
}
newCur->random = map[pos];
}
cur = cur->next;
newCur = newCur->next;
}
return newHead;
}
方法二:节点拆分法(最优解)
c
/**
* 随机链表的复制 - 节点拆分法
* @param head: 原链表头指针
* @return: 复制链表的头指针
*
* 思路:
* 1. 复制节点:在每个原节点后面插入一个值相同的新节点
* 原链表:A -> B -> C
* 变成:A -> A' -> B -> B' -> C -> C'
*
* 2. 设置random指针:
* 原节点的random指向的节点,其下一个节点就是新节点random应该指向的
* 即:A'.random = A.random->next
*
* 3. 拆分链表:将原链表和新链表分开
*
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
RandomListNode* copyRandomList_optimal(RandomListNode* head) {
if (head == NULL) return NULL;
// 第一步:复制节点,插入到原节点后面
RandomListNode* cur = head;
while (cur != NULL) {
// 创建新节点
RandomListNode* copy = (RandomListNode*)malloc(sizeof(RandomListNode));
copy->val = cur->val;
copy->next = cur->next; // 新节点的next指向原节点的下一个
copy->random = NULL; // random暂不设置
// 将新节点插入到cur后面
cur->next = copy;
// 移动到下一个原节点
cur = copy->next;
}
// 第二步:设置random指针
cur = head;
while (cur != NULL) {
// cur的下一个节点就是对应的新节点
RandomListNode* copy = cur->next;
// 如果原节点的random不为空,则新节点的random应该指向原节点random的下一个节点
if (cur->random != NULL) {
copy->random = cur->random->next;
}
// 移动到下一个原节点(跳过新节点)
cur = copy->next;
}
// 第三步:拆分链表
RandomListNode* newHead = head->next; // 新链表的头节点
RandomListNode* newCur = newHead;
cur = head;
while (cur != NULL) {
// 恢复原链表的next指针
cur->next = newCur->next;
// 设置新链表的next指针
if (newCur->next != NULL) {
newCur->next = newCur->next->next;
}
// 移动到下一个节点
cur = cur->next;
newCur = newCur->next;
}
return newHead;
}
辅助函数
c
/**
* 创建随机链表节点
*/
RandomListNode* createRandomNode(int val) {
RandomListNode* node = (RandomListNode*)malloc(sizeof(RandomListNode));
if (node != NULL) {
node->val = val;
node->next = NULL;
node->random = NULL;
}
return node;
}
/**
* 根据数组创建随机链表(random暂时为NULL)
*/
RandomListNode* createRandomList(int* arr, int size) {
if (size == 0) return NULL;
RandomListNode* head = createRandomNode(arr[0]);
RandomListNode* cur = head;
for (int i = 1; i < size; i++) {
cur->next = createRandomNode(arr[i]);
cur = cur->next;
}
return head;
}
/**
* 设置random指针(通过位置)
*/
void setRandomByIndex(RandomListNode* head, int* randomIndices, int size) {
// 先获取所有节点的指针
RandomListNode** nodes = (RandomListNode**)malloc(sizeof(RandomListNode*) * size);
RandomListNode* cur = head;
for (int i = 0; i < size; i++) {
nodes[i] = cur;
cur = cur->next;
}
// 设置random指针
cur = head;
for (int i = 0; i < size; i++) {
if (randomIndices[i] != -1) {
nodes[i]->random = nodes[randomIndices[i]];
}
cur = cur->next;
}
free(nodes);
}
/**
* 打印随机链表
*/
void printRandomList(RandomListNode* head) {
printf("随机链表:\n");
int index = 0;
while (head != NULL) {
printf("节点%d: val=%d", index, head->val);
if (head->random != NULL) {
printf(", random指向值=%d", head->random->val);
} else {
printf(", random指向null");
}
printf("\n");
head = head->next;
index++;
}
}
/**
* 释放随机链表
*/
void freeRandomList(RandomListNode* head) {
while (head != NULL) {
RandomListNode* temp = head;
head = head->next;
free(temp);
}
}
测试代码
c
// 测试随机链表的复制
void testRandomListCopy() {
printf("\n=== 测试随机链表的复制 ===\n");
// 创建示例链表:[[7,null],[13,0],[11,4],[10,2],[1,0]]
int values[] = {7, 13, 11, 10, 1};
int randomIndices[] = {-1, 0, 4, 2, 0}; // -1表示null
int size = sizeof(values) / sizeof(values[0]);
RandomListNode* head = createRandomList(values, size);
setRandomByIndex(head, randomIndices, size);
printf("原链表:\n");
printRandomList(head);
// 复制链表
RandomListNode* copy = copyRandomList_optimal(head);
printf("\n复制后的链表:\n");
printRandomList(copy);
// 验证复制是否正确(修改原链表不影响复制)
if (head != NULL && head->next != NULL) {
head->next->val = 999; // 修改原链表
}
printf("\n修改原链表后:\n");
printf("原链表: 节点1的值=%d\n", head->next->val);
printf("复制链表: 节点1的值=%d\n", copy->next->val);
freeRandomList(head);
freeRandomList(copy);
}
总结与归纳
各题核心要点回顾
1. 轮转数组
- 取模处理 :
k = k % numsSize处理k大于数组长度的情况 - 三次反转:原地算法,空间复杂度O(1)
- 环状替换:数学方法,理解循环节的概念
2. 消失的数字
- 数学求和:注意整数溢出,使用long类型
- 异或运算 :
a^a=0, a^0=a,不需要考虑溢出 - 位运算优势:比加减法更快,不会溢出
3. 返回倒数第k个节点
- 快慢指针:经典技巧,一次遍历解决问题
- 边界处理:k可能大于链表长度
- 双指针应用:找中点、找环入口等都可以用
4. 链表的回文结构
- 快慢找中点 :
fast->next && fast->next->next区分奇偶 - 链表反转:三指针法迭代反转
- 恢复原链表:好习惯,避免破坏输入
5. 相交链表
- 相遇:双指针走完自己的路再走对方的路
- 地址比较:比较节点本身,不是比较值
- 长度差法:另一种思路,先对齐再比较
6. 随机链表的复制
- 节点插入:在原节点后面插入新节点
- 三步走:复制->设置random->拆分
- 原地算法:不需要额外空间
C语言实现注意事项
-
内存管理
- malloc后必须检查是否为NULL
- 使用后必须free,避免内存泄漏
- 注意野指针问题
-
指针操作
- 操作链表时总是检查NULL
- 修改next指针前先保存下一个节点
- 区分
p = p->next和p->next = q
-
边界条件
- 空链表处理
- 单节点链表处理
- k等于0或等于长度的处理
-
性能优化
- 使用局部变量减少指针访问
- 一次遍历能解决的问题不要两次
- 注意递归深度,避免栈溢出
进阶思考
-
如果链表有环怎么办?
- 相交链表:需要先判断环
- 回文链表:有环就不能用原方法
-
如果内存受限怎么办?
- 优先考虑原地算法
- 使用流式处理,分批读取
-
如果是并发环境?
- 考虑线程安全
- 避免使用全局变量
最后,感谢各位大佬的观看!