双指针法寻找单链表中间结点@单链表逆序

单链表逆序(逆置)

逆序操作是单链表操作中比较复杂的操作

设序列为 a 1 , ⋯   , a n a_{1},\cdots,a_{n} a1,,an,逆序得到 a n , ⋯   , a 1 a_{n},\cdots,a_{1} an,,a1,从结果上看, a i a_{i} ai的后继为 a i − 1 a_{i-1} ai1( i = n , n − 1 , ⋯   , 2 i=n,n-1,\cdots,2 i=n,n1,,2)

基本思路遍历原序列时,将 a i + 1 a_{i+1} ai+1的后继设置为 a i a_{i} ai,利用循环推进 i i i重复此流程

为了简单起见,假设链表长度足够长

p p p a i a_{i} ai的指针,则p->next a i + 1 a_{i+1} ai+1的指针,如果执行 a i + 1 → a i a_{i+1}\to{a_{i}} ai+1ai,则p->next=p,此时是 a i + 1 a_{i+1} ai+1将继续无法访问(原本靠p->next访问的 a i + 1 a_{i+1} ai+1不可用了),因此需要借助其他辅助指针;

让第二个指针q=p->next,表示qp的后继,这样 a i + 1 → a i a_{i+1}\to{a_{i}} ai+1ai a i + 1 a_{i+1} ai+1仍然可以通过q访问,然而 a i + 2 a_{i+2} ai+2不能访问了,所以还需要第三个赋值指针变量r;

令第三个指针r=q->next,表示rq的后继

则当p a i a_{i} ai的指针时,q,r分别是 a i + 1 , a i + 2 a_{i+1},a_{i+2} ai+1,ai+2;如此 a i + 1 → a i a_{i+1}\to{a_{i}} ai+1ai也不会造成后续 a i + 2 , ⋯ a_{i+2},\cdots ai+2,结点无法访问

a i + 1 → a i a_{i+1}\to{a_{i}} ai+1ai完成后,为了继续将 a i + 2 → a i + 1 a_{i+2}\to{a_{i+1}} ai+2ai+1,需要更新三个指针p,q,r;

p的移动顺序: a 1 , a 2 , ⋯ a_{1},a_{2},\cdots a1,a2,

q的移动顺序 a 2 , a 3 , ⋯ a_{2},a_{3},\cdots a2,a3,

r的移动顺序: a 3 , a 4 ⋯ a_{3},a_{4}\cdots a3,a4

看似p,q,r只是是前驱后继的关系,在逆序过程中, a i , a i + 1 a_{i},a_{i+1} ai,ai+1不一定总是前驱后继的关系,算法中是要把q的后继改为p,创建对应辅助指针时p是用来指示已经被逆序的部分链表的首元指针,随着逆序工作的推进,p会更新为q(和p=p->next不同,p的后继结点是朝着 a 1 a_{1} a1的方向去的),q会更新为r

//初始化
p=head;
q=p->next;
r=q->next;
p=q;//更新已逆序部分的首指针(作为头插法的着陆点)
q=r;//未逆序部分的首指针
r=r->next;//未逆序部分的第二结点指针

算法代码描述

为了便于算法描述,需要做一些调整和非空检查,代码如下

// 逆序链表函数
void reverseList(Node *head)
{
    Node *prev = NULL;          // 指示被逆序部分的新序列(链表)的表头(从NULL开始,进行头插法);刚开始时没有元素完成逆序,因此已被逆序的部分为空
    Node *current = head->next; // 指示逆序进度(当前要被逆序的结点),跳过头结点(从首元开始)
    Node *next = NULL;          // 作为current的后继(能够直接初始化为next=current->next),因为首元结点还没判断是否非空;

    // prev,next在第一次循环中进行第一次更新值
    while (current != NULL)
    {
        next = current->next; // 保存下一个节点
        current->next = prev; // 反转指针(将current的后继指针指向prev,current结点的逆序就完成了);注意第一个被反转的结点将作为新序列的尾结点,其后继为NULL,循环的主要工作是从NULL(尾结点开始)找到前驱,然后修改前驱结点的后继指针;
        // 推进进度并维护指针间的逻辑关系
        prev = current; // 更新prev指针(pre新序列的表头)

        current = next; // 向后移动current指针
    
        // next=next->next;//这个语句需要确保next不是NULL,否则会报错;此外,前面next赋值给了current,next=next->next;等价于next=current->next;然而这需要确保current不是NULL,否则会报错,而对于循环而言,while(current!=NULL)正好能做这个检查,所以这个工作就留给下一次循环处理
    }//循环的核心是指针反转current->next=prev和current=next推进逆序进度,其余语句维护指针的语言(逻辑关系)以便进行下一个循环

    head->next = prev; // 头结点指向新的头部(原尾节点)
}

把啰嗦的注释清理一部分

// 逆序链表函数
void reverseList(Node *head)
{
    Node *prev = NULL;          
    Node *current = head->next; // 指示逆序进度(当前要被逆序的结点),跳过头结点(从首元开始)
    Node *next = NULL;          

    while (current != NULL)
    {
        next = current->next; // 保存下一个节点
        current->next = prev; // 反转指针
        prev = current; // 更新prev指针(pre新序列的表头)
        current = next; // 向后移动current指针
    

    }//循环的核心是指针反转current->next=prev和current=next推进逆序进度,其余语句维护指针的语言(逻辑关系)以便进行下一个循环

    head->next = prev; // 头结点指向新的头部(原尾节点)
}

可执行代码

#include <stdio.h>
#include <stdlib.h>

// 定义链表节点结构
typedef struct Node
{
    int data;
    struct Node *next;
} Node;

// 初始化链表并从数组创建链表
Node *createList(int arr[], int size)
{
    // 使用尾插法创建链表,创建链表的时间复杂度为O(n),一个尾指针来辅助(否则时间复杂度会上升值O(n^2))
    Node *head = (Node *)malloc(sizeof(Node)); // 创建头结点
    head->next = NULL;                         // 初始化头结点的next指针为空

    Node *current = head; // 定义辅助的当前指针(或称为尾指针),初始化为指向头结点

    for (int i = 0; i < size; i++)
    {
        Node *newNode = (Node *)malloc(sizeof(Node)); // 创建新节点
        newNode->data = arr[i];                       // 设置数据
        newNode->next = NULL;
        current->next = newNode; // 链接新节点到链表尾部
        current = newNode;       // 移动当前指针到新节点
    }

    return head; // 返回带头结点的链表
}

// 逆序链表函数
void reverseList(Node *head)
{
    Node *prev = NULL;          // 指示被逆序部分的新序列(链表)的表头(从NULL开始);刚开始时没有元素完成逆序,因此已被逆序的部分为空
    Node *current = head->next; // 指示逆序进度(当前要被逆序的结点),跳过头结点(从首元开始)
    Node *next = NULL;          // 作为current的后继(能够直接初始化为next=current->next),因为首元结点还没判断是否非空;

    // prev,next在第一次循环中进行第一次更新值
    while (current != NULL)
    {
        next = current->next; // 保存下一个节点
        current->next = prev; // 反转指针(将current的后继指针指向prev,current结点的逆序就完成了);注意第一个被反转的结点将作为新序列的尾结点,其后继为NULL,循环的主要工作是从NULL(尾结点开始)找到前驱,然后修改前驱结点的后继指针;
        // 推进进度并维护指针间的逻辑关系
        prev = current; // 更新prev指针(pre新序列的表头)

        current = next; // 向后移动current指针
    
        // next=next->next;//这个语句需要确保next不是NULL,否则会报错;此外,前面next赋值给了current,next=next->next;等价于next=current->next;然而这需要确保current不是NULL,否则会报错,而对于循环而言,while(current!=NULL)正好能做这个检查,所以这个工作就留给下一次循环处理
    }//循环的核心是指针反转current->next=prev和current=next推进逆序进度,其余语句维护指针的语言(逻辑关系)以便进行下一个循环

    head->next = prev; // 头结点指向新的头部(原尾节点)
}
void reverseList2(Node *head)
{
    // 逆序过程中分为两部分,一部分是已经逆序处理的部分,另一部分是待逆序的部分

    Node *p = head->next; //
    Node *q = p->next;
    Node *r = q->next;
    while (p->next)
    {
        /* code */
    }
}
// 打印链表函数
void printList(Node *head)
{
    Node *current = head->next; // 跳过头结点
    while (current != NULL)
    {
        printf("%d ", current->data);
        current = current->next;
    }
    printf("\n");
}

// 释放链表内存
void freeList(Node *head)
{
    while (head != NULL)
    {
        Node *temp = head;
        head = head->next;
        free(temp); // 释放节点内存
    }
}

// 测试函数
int main()
{
    int arr[] = {1, 2, 3, 4, 5};             // 初始化数组
    int size = sizeof(arr) / sizeof(arr[0]); // 计算数组大小

    Node *head = createList(arr, size); // 创建链表

    printf("原链表: ");
    printList(head);

    reverseList(head); // 逆序链表

    printf("逆序后的链表: ");
    printList(head);

    freeList(head); // 释放链表内存

    return 0;
}

确定单链表中间结点

确定链表的中间指针,可以使用双指针,让两个指针在同一个循环中前进,快指针每次前进2步,慢指针每次前进1步,直到快指针走到尽头为止

// 寻找单链表的中间节点
NODE *findMiddleNode(NODE *head)
{
    NODE *slow = head->next; // 慢指针从头结点后面开始
    NODE *fast = head->next; // 快指针从头结点后面开始

    while (fast != NULL && fast->next != NULL)//fast是尾结点时退出循环
    {
        slow = slow->next;
        fast = fast->next->next;
    }

    return slow;
}


可执行代码

#include <stdio.h>
#include <stdlib.h>

// 定义链表节点结构
typedef struct Node
{
    int data;
    struct Node *next;
} Node;

// 寻找单链表的中间节点
Node *findMiddleNode(Node *head)
{
    Node *slow = head->next; // 慢指针从头结点后面开始
    Node *fast = head->next; // 快指针从头结点后面开始

    while (fast != NULL && fast->next != NULL)
    {
        slow = slow->next;
        fast = fast->next->next;
    }

    return slow;
}

// 初始化链表并从数组创建链表
Node *createList(int arr[], int size)
{

    Node *head = (Node *)malloc(sizeof(Node)); // 创建头结点
    head->next = NULL;                         // 初始化头结点的next指针为空

    Node *current = head; // 定义辅助的当前指针(或称为尾指针),初始化为指向头结点

    for (int i = 0; i < size; i++)
    {
        Node *newNode = (Node *)malloc(sizeof(Node)); // 创建新节点
        newNode->data = arr[i];                       // 设置数据
        newNode->next = NULL;
        current->next = newNode; // 链接新节点到链表尾部
        current = newNode;       // 移动当前指针到新节点
    }

    return head; // 返回带头结点的链表
}
int main(int argc, char const *argv[])
{
    int arr[] = {1, 2, 3, 4, 5};             // 初始化数组
    int size = sizeof(arr) / sizeof(arr[0]); // 计算数组大小

    Node *head = createList(arr, size); // 创建链表

    Node *middle = findMiddleNode(head);
    printf("中间节点的数据: %d\n", middle->data);
    freeList(head); // 释放链表内存
    return 0;
}

综合应用

先观察 L = ( a 1 , a 2 , a 3 , ⋯ , a n − 2 , a n − 1 , a n ) L=(a_{1},a_{2},a_{3},⋯,a_{n-2},a_{n-1},a_n) L=(a1,a2,a3,,an2,an1,an) L ′ = ( a 1 , a n , a 2 , a n − 1 , a 3 , a n − 2 , ⋯   ) L'=(a_1,a_n,a_2,a_{n-1},a_3,a_{n-2},\cdots) L=(a1,an,a2,an1,a3,an2,)

可以发现 L ′ L' L是由 L L L摘取第一个元素,再摘取倒数第一个元素⋯依次合并而成的。

用公式描述,就是将 a n − k + 1 a_{n-k+1} ank+1插入到 i = 2 k i=2k i=2k的位置( k = 1 , 2 , ⋯   , [ n / 2 ] k=1,2,\cdots,[n/2] k=1,2,,[n/2])

虽然访问 i = 2 k i=2k i=2k的位置相对容易(只需要扫描一遍,可以顺路访问到)

然而逆序访问数据(依次访问 a n , a n − 1 , ⋯ a_{n},a_{n-1},\cdots an,an1,对于链表来说是麻烦和低效率的,如果将序列 L L L中的后半部分逆序,可使得访问 a n , a n − 1 ⋯   , a [ n 2 ] + 1 a_{n},a_{n-1}\cdots,a_{[\frac{n}{2}]+1} an,an1,a[2n]+1不需要重复扫描

为了方便链表后半段取元素,需要先将L后半段原地逆置,否则每取最后一个节点都需要遍历一次链表。

  1. 先找出链表L中的中间节点,为此设置两个指针p和q。指针p每次走一步,指针q每次走两步,当指针q到达链尾时,指针p正好在链表的中间节点
  2. 然后将L的后半段节点原地逆置;(方法有多种,主要依赖于多个指针辅助)
  3. 从单链表前后两段中依次各取一个节点,按要求重排。

重排算法实现

下面的片段提供大致的实现,但是部分细节有漏洞,无法实际运行(可运行代码完整版另见下一节可执行代码)

void change_list(NODE*h){
    NODE *p,*q,*r,*s;//定义4个辅助指针;
    p=q=h;//将p,q都初始化为h
    //q前进到速度是p的两倍
    while(q->next!=NULL){ //寻找中间节点
        p=p->next;
        q=q->next;
        //为了避免q->next为NULL,q执行第二步前进需要再判断q->next!=NULL,才能再前进一步
        if(q->next!=NULL) q=q->next; //条件允许时q再走一步
    }//至此,p->next是后半部序列的首元,而q是尾元
    
    q=p->next; //p所指节点为中间节点,其后继赋值给q为后半段链表的首节点
    p->next=NULL;//中间结点的后继置为NULL,即L前半段的尾指针的后继为NULL

    while(q!=NULL){ //将链表后半段逆置
        r=q->next;//q的后继赋值给辅助指针r
        q->next=p;
        p=q;
        q=r;
    }
    s=h->next; //s指向前半段的第一个数据节点,即插入点
    q=p->next; //指向后半段的第一个数据节点

    while(q!=NULL){ //将链表后半段的节点插入到指定位置
        r=q->next; //指向后半段的下一个节点
        q->next=s->next; //将q所指节点插入到s所指节点之后
        s->next=q;
        s=q->next; //s指向前半段的下一个插入点
        q=r;
    }
}

第一步找中间节点的时间复杂度为O(n),第二步逆置的时间复杂度为O(n),第三步合并链表的时间复杂度为O(n),所以该算法的时间复杂度为O(n)。

可执行代码

代码和之前的可执行代码有所重复,为了便于读者验证,放在了同一个片段里;

#include <stdio.h>
#include <stdlib.h>

// 定义链表节点结构
typedef struct Node
{
    int data;
    struct Node *next;
} Node;

// 初始化链表并从数组创建链表
Node *createList(int arr[], int size)
{
    // 使用尾插法创建链表,创建链表的时间复杂度为O(n),一个尾指针来辅助(否则时间复杂度会上升值O(n^2))
    Node *head = (Node *)malloc(sizeof(Node)); // 创建头结点
    head->next = NULL;                         // 初始化头结点的next指针为空

    Node *current = head; // 定义辅助的当前指针(或称为尾指针),初始化为指向头结点

    for (int i = 0; i < size; i++)
    {
        Node *newNode = (Node *)malloc(sizeof(Node)); // 创建新节点
        newNode->data = arr[i];                       // 设置数据
        newNode->next = NULL;
        current->next = newNode; // 链接新节点到链表尾部
        current = newNode;       // 移动当前指针到新节点
    }

    return head; // 返回带头结点的链表
}


// 打印链表函数
void printList(Node *head)
{
    Node *current = head->next; // 跳过头结点
    while (current != NULL)
    {
        printf("%d ", current->data);
        current = current->next;
    }
    printf("\n");
}
void printList_NoHeader(Node *head)
{
    Node *current = head; // 跳过头结点
    while (current != NULL)
    {
        printf("%d ", current->data);
        current = current->next;
    }
    printf("\n");
}
// 释放链表内存
void freeList(Node *head)
{
    while (head != NULL)
    {
        Node *temp = head;
        head = head->next;
        free(temp); // 释放节点内存
    }
}


void change_list(Node *h)
{
    Node *p, *q, *r, *s; // 定义4个辅助指针;(前2个确定中间结点和尾结点);r辅助逆序后半段链表,s用于后边段链表的遍历指针
    p = q = h;           // 将p,q都初始化为h
    printList(h);
    // q前进到速度是p的两倍(p是慢指针,q是快指针)
    while (q->next != NULL)
    { // 寻找中间节点(这里p在结点数量为奇数时n/2+1)
        p = p->next;
        q = q->next;
        // print p,q
        // printf("p,q: %d,%d\n", p->data, q->data);
        // 为了避免q->next为NULL,q执行第二步前进需要再判断q->next!=NULL,才能再前进一步
        if (q->next != NULL)
            q = q->next; // 条件允许时q再走一步
        // printf("p,q: %d,%d\n", p->data, q->data);
        // printf("\tq:%d\n", q->data);
    } // 至此,p->next是后半部序列的首元,而q是尾元
    // printList_NoHeader(p->next);
    // printList(h);
    q = p->next;    // p所指节点为中间节点,其后继赋值给q为后半段链表的首节点
    p->next = NULL; // 中间结点的后继置为NULL,即L前半段的尾指针的后继为NULL;此后p作为已逆序的部分链表的首元指针,作为头插法着陆点

    while (q != NULL)
    { // 将链表后半段逆置

        r = q->next; // q的后继赋值给辅助指针r
        q->next = p; // 未逆序的部分链表首元后继改为已逆序部分的首元
        // 维护指针间的逻辑关系,以便下一轮操作
        p = q; // 迭代已逆序部分的首元指针
        q = r; // 迭代未逆序部分的首元指针
    } // p此时指向后半段链表的首元(尾元是前半段的尾元(重叠))
    printf("reverse result of first half list:");
    printList(h);
    printf("reverse result of later half list:");
    printList_NoHeader(p);
    /* 准备重排 */
    // 准备两段链表的首元指针
    s = h->next; // s指向前半段的第一个数据节点,即插入点
    q = p;       // 指向后半段的第一个数据节点

    while (s->next != NULL) // 第一个链表长度可能短一个元素(总链表长度为偶数的时候);为了防止循环内赋值空指针造成segment fault(运行时难以察觉的错误),这里用s->next!=NULL判断是否进入循环;
    {                       // 将链表后半段的节点插入到指定位置
        r = q->next;        // 将q插入到前半段链表前,标记q的后继,即指定指向后半段的下一个节点备用
        q->next = s->next;  // s后继交接给q:将q所指节点插入到s所指节点之后
        s->next = q;        // s的新后继为q;
        printf("s,q: %d,%d\n", s->data, q->data);
        // 更新下一个带插入结点的前驱
        s = q->next; // s指向前半段的下一个插入点
        q = r;       // 更新第二段链表中下一个要插入的节点(第二段链表的首元)
        // print s,q
        // printf("s,q: %d,%d\n", s->data, q->data);
    }
    // 退出时第二段链表可能尾结点多出来了, 实际上两段链表尾结点重合(相同),检查第一段尾结点取值和第二段相同位序结点q数据是否相同,如果不同,那么就接上,并且尾结点置空,否则就不用接了(第一段的尾结点作为做终链表的尾结点,其后继为NULL)
    if (s->data != q->data)
    {

        q->next = NULL;
        s->next = q;
    }
    // if()
    printf("resort res:\n");
    printList(h);
}

int main(int argc, char const *argv[])
{
    // int arr[] = {1, 2, 3, 4, 5, 6, 7, 8}; // 初始化数组(偶数个结点)
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; //奇数个结点

    int size = sizeof(arr) / sizeof(arr[0]); // 计算数组大小
    Node *head = createList(arr, size);      // 创建链表

    change_list(head);
    // printList(head);
    freeList(head); // 释放链表内存
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cxxu1375

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值