数据结构与算法(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个节点,可以用两个指针法

  1. 定义两个指针 fastslow,都指向首元节点(不是头结点)。
  2. 先让 fast 向前移动 k 步,这样 fastslow 之间的距离就为 k
  3. 然后两个指针同时往后移动,当 fast 到达链表末尾时,slow 所指的位置就是倒数第 k 个节点
  4. 如果 fast 提前到达 NULL,说明链表长度小于 k,返回 0。

该算法只需遍历一遍链表,时间复杂度 O(n),空间复杂度 O(1)。


二,算法详细实现步骤

  1. 用两个指针 fastslow 指向首元节点;
  2. fast 向前移动 k 步,如果此时 fast == NULL,说明 k 大于链表长度,返回 0;
  3. 否则同时移动 fastslow,直到 fast == NULL
  4. 此时 slow 即为倒数第 k 个节点;
  5. 输出其 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、找相同后缀

核心思路:

  1. 分别求出两个链表的长度m和n

  1. fast指针指向较长的链表,其先走m-n或者n-m步

  1. 同步移动指针,判断他们是否指向同一节点.当指向同一节点时,找到了

核心思路:

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;
}

做题目:

  1. 基本设计思路
  • 设两条带头结点的单链表分别为 str1str2,首元结点为 str1->nextstr2->next
  • 先分别求出两表长度 m、n(不含头结点)。
  • 让较长表的指针先前进 |m-n| 步,使两指针到尾部的剩余长度相同(对齐)。
  • 然后两指针同步前进,第一次出现 指针地址相等 的结点即为“共享后缀”的起点;若直到 NULL 都未相等,则不存在共享后缀。

关键点:判断“同一结点”必须比较指针地址p1 == p2),不能比较 data


  1. 代码实现(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->g
    Node *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->g
    Node *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);
    else
        printf("无公共后缀\n");

    return 0;
}

  1. 时间复杂度说明
  • 计算两个长度:各遍历一次,代价 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]

    // 初始化标记数组为 0
    for (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->next
            Node *temp = p->next;
            p->next = temp->next;
            free(temp);
        }
    }
    free(q);
}
  1. 基本设计思路
  • 题设给出:链表存 n 个整数,且对任一结点 |data| ≤ n
  • 开一个标记数组 seen[0..n],初值全 0。
  • 设指针 p头结点开始,始终保持 p 是“待检查结点的前驱”。
    • index = abs(p->next->data)
    • seen[index] == 0:说明该绝对值第一次出现,置 seen[index]=1p = p->next
    • 否则:该绝对值重复,删除 p->nextp->next = p->next->next; free(被删结点)),p 不动
  • 一趟扫描完成。
  1. 结点数据类型定义
typedef struct Node {
    int data;            // 结点的整数值,可能为负,且 |data| ≤ n
    struct Node *next;   // 指向后继
} Node;
  1. C 代码
#include <stdio.h>
#include <stdlib.h>   // malloc, free
#include <math.h>     // abs

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){
    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->next
            Node *temp = p->next;
            p->next = temp->next;
            free(temp);
        }
    }
    free(seen);
}

int main(){
    // head: 21 -> -15 -> -15 -> -7 -> 15
    Node *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 -7
    return 0;
}
  1. 复杂度
  • 时间复杂度:一次线性扫描,每个结点 O(1) 处理 ⇒ O(n)(这里 n 指结点个数)。
  • 空间复杂度:标记数组 seen[0..n](这里的 n 为数据上界) ⇒ O(n)

4、反转链表

思路

  1. first指向空值,second指向1,third指向second的下一个节点

  1. 用second指向空值,然后挪,然后再让second指回1,再挪

  1. 直到second指向NULL,再加个头节点


```c
Node* reverseList(Node *head) {
    Node *first = NULL;         // 已反转部分的头
    Node *second = head->next;  // 待反转部分
    Node *third;                // 暂存下一节点

    while (second != NULL) {
        third = second->next;   // 暂存 next
        second->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;   // 暂存 next
        second->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、删除链表中间节点

  1. fast指向1,slow指向head

  1. fast走两步(fast=fast->next->next),slow走一步(slow=slow->next)

  1. 发现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、做个题目练练

效果如图所示:

  1. 找到中间的位置,断开

  1. 把后半截反转

  1. 6插到1和2中间,5插到2和3中间,4放到3后面.

利用四个指针 p1p2q1q2 实现两条链表的交叉合并。p1 指向第一条链表当前节点, p2 指向 p1 的下一个节点; q1 指向第二条链表当前节点, q2 指向 q1 的下一个节点。每次操作时,先将 q1 插入到 p1p2 之间(即 p1->next=q1q1->next=p2),使第二条链表当前节点嵌入第一条链表对应位置;然后四个指针整体后移(p1=p2q1=q2),继续进行下一轮插入。如此循环,直到任意一条链表遍历完为止,就能让两条链表的节点像拉链一样交错连接成一个完整的新链表

也就是

初始状态

A链表: Head → 123NULL  
B链表: 654NULL  

指针:
p1 → 1
p2 → 2
q1 → 6
q2 → 5

下一步:把6插到1和2中间

操作:
p1->next = q1
q1->next = p2

结果:
Head → 1623
B剩余:54

更新指针:
p1 → 2
p2 → 3
q1 → 5
q2 → 4

下一步:把5插到2和3之间

操作:
p1->next = q1
q1->next = p2

结果:
Head → 16253
B剩余:4

更新指针:
p1 → 3
p2 → NULL
q1 → 4
q2 → NULL

下一步: 把4插到3后面

操作:
p1->next = q1
q1->next = p2 (此时p2为NULL)

结果:
Head → 162534NULL

更新指针:
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、单链表的局限:

不可以回头

于是想个办法:

  1. 单向循环链表

循环链表(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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值