数据结构与算法经典OJ题目详解(C语言):从数组到链表的进阶之路(上)

个人主页:
wengqidaifeng

✨ 永远在路上,永远向前走

个人专栏:
数据结构
C语言
嵌入式小白启动!

文章目录


前言

本文使用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语言实现注意事项

  1. 内存管理

    • malloc后必须检查是否为NULL
    • 使用后必须free,避免内存泄漏
    • 注意野指针问题
  2. 指针操作

    • 操作链表时总是检查NULL
    • 修改next指针前先保存下一个节点
    • 区分p = p->nextp->next = q
  3. 边界条件

    • 空链表处理
    • 单节点链表处理
    • k等于0或等于长度的处理
  4. 性能优化

    • 使用局部变量减少指针访问
    • 一次遍历能解决的问题不要两次
    • 注意递归深度,避免栈溢出

进阶思考

  1. 如果链表有环怎么办?

    • 相交链表:需要先判断环
    • 回文链表:有环就不能用原方法
  2. 如果内存受限怎么办?

    • 优先考虑原地算法
    • 使用流式处理,分批读取
  3. 如果是并发环境?

    • 考虑线程安全
    • 避免使用全局变量

最后,感谢各位大佬的观看!

相关推荐
爱编码的小八嘎1 小时前
第3章 Windows运行机理-3.1 内核分析(10)
c语言
祈安_2 小时前
深入理解指针(七)
c语言·后端
blackicexs2 小时前
第六周第一天
数据结构·算法
We་ct3 小时前
LeetCode 236. 二叉树的最近公共祖先:两种解法详解(递归+迭代)
前端·数据结构·算法·leetcode·typescript
历程里程碑3 小时前
普通数组---合并区间
java·大数据·数据结构·算法·leetcode·elasticsearch·搜索引擎
无忧.芙桃4 小时前
AVL树的实现
数据结构·c++
m0_531237174 小时前
C语言-编程实例2
c语言·开发语言
踢足球09295 小时前
寒假打卡:2026-2-23
数据结构·算法
zjxtxdy5 小时前
C语言(续)
c语言·开发语言