数据结构:链表基础OJ练习+带头双向循环链表的实现

这篇具有很好参考价值的文章主要介绍了数据结构:链表基础OJ练习+带头双向循环链表的实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

一.leetcode剑指 Offer II 027. 回文链表

1.问题描述

2.问题分析与求解

(1) 快慢指针法定位链表的中间节点

(2) 将链表后半部分进行反转

附:递归法反转链表

(3) 双指针法判断链表是否回文

二.带头双向循环链表的实现

1.头文件

2.节点内存申请接口和链表初始化接口

3.链表的打印和查找接口

4.链表的增删接口

5.链表销毁接口


一.leetcode剑指 Offer II 027. 回文链表

剑指 Offer II 027. 回文链表 - 力扣(Leetcode)

1.问题描述

给定一个链表的头节点head,请判断其是否为回文链表。(是回文链表则程序返回true,不是回文链表则程序返回false)

如果一个链表是回文,那么链表节点序列从前往后看从后往前看是相同的

数据结构:链表基础OJ练习+带头双向循环链表的实现

题解接口:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution 
{
public:
    bool isPalindrome(ListNode* head) 
    {

    }
};

2.问题分析与求解

如果要求题解的时间复杂度为O(N),空间复杂度为O(1),那么本题的求解要分为三个部分:

  1. 用快慢指针法找到链表中间位置节点
  2. 将链表后半部分进行反转
  3. 用双指针法将链表前半部分和后半部分进行比对来判断链表是否回文

(1) 快慢指针法定位链表的中间节点

  • 思路是两个指针同时遍历链表,快指针一次走两步(fast=fast->next->next),慢指针一次走一步(slow = slow->next)。

数据结构:链表基础OJ练习+带头双向循环链表的实现

  • 当快指针结束遍历时,慢指针恰好会指向中间位置的节点(对于奇数个节点的链表而言,慢指针最后会指向中间节点,对于偶数个节点的链表而言,慢指针最后会指向中间两个节点的第二个节点)数据结构:链表基础OJ练习+带头双向循环链表的实现

寻找中间位置节点的接口:

    ListNode * FindMid(ListNode * head)
    {
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast && fast->next)   //注意循环的限制条件
        {
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }
  • 我们以偶数个节点链表的情况为例,简单地证明一下慢指针最后会指向中间两个节点的第二个节点:数据结构:链表基础OJ练习+带头双向循环链表的实现

对于奇数个节点链表的情况也可以作相似的证明。 

(2) 将链表后半部分进行反转

  • 定位链表中间位置节点后,我们便可以将链表的后半部分进行反转.

数据结构:链表基础OJ练习+带头双向循环链表的实现

  • 完成链表反转的最优方法是三指针反转法(动画):数据结构:链表基础OJ练习+带头双向循环链表的实现

三指针反转链表

的接口:

    ListNode * reverse (ListNode * head)
    {
        ListNode * cur = (nullptr == head)? nullptr : head->next;
        ListNode * pre = head;
        while(cur)
        {
            ListNode* Next = cur->next;
            cur->next = pre;
            pre = cur;
            cur = Next;
        }
        if(head)
        head->next = nullptr;   //记得将被反转部分链表的尾结点的指针域置空
        return pre;             //pre最终指向反转链表的表头
    }

附:递归法反转链表

递归算法经常出现在单链表问题的题解中,其原因在于:递归算法可以利用多个函数栈帧来存储每一个链表节点的地址(而单链表的缺陷正是在于寻址困难),所以递归算法经常作为单链表问题的可行解之一.(但是递归算法由于压栈开销较大,往往并不是最优解,比如递归法反转链表在时间和空间上的开销都要比三指针反转法更大)

然而以思维训练和加深对递归的理解为目的,这里尝试着解构一下递归反转单链表的算法。

反转单链表递归函数的建立:

  • 递归反向遍历链表节点的框架:
        ListNode* reverseList (ListNode * head)
        {
            if(head->next == nullptr)//递归的结束条件
            {
                return head;
            }
            reverseList(head->next);
            return head;   
        }

    递归框架可以实现反向遍历单链表(图解)数据结构:链表基础OJ练习+带头双向循环链表的实现

  • 在递归函数反向遍历链表节点的过程中我们可以加入修改节点指针域的操作
    ListNode* reverseList (ListNode * head)
    {
        if(head->next == nullptr)//递归的结束条件
        {
            return head;
        }
        reverseList(head->next);
        head->next->next = head; 
        head->next = nullptr;
        return head;
    }

递归函数修改节点指针域过程动画解析:

数据结构:链表基础OJ练习+带头双向循环链表的实现

  • 我们希望函数能够将反转后链表新头节点的地址作为最终返回值带回:
        ListNode* reverseList (ListNode * head)
        {
            if(head->next == nullptr)//递归的结束条件
            {
                return head;
            }
            ListNode* newhead = reverseList(head->next); //利用newhead将新的头节点地址逐层带回
            head->next->next = head; 
            head->next = nullptr;
            return newhead;
        }

    递归函数将新的头节点地址逐层带回的过程图解:数据结构:链表基础OJ练习+带头双向循环链表的实现

  • 递归反转单链表的接口:

        ListNode* reverseList(ListNode* head) 
        {
            if(nullptr == head || nullptr == head->next)
            //设置递归的限制条件,构建递归框架
            {
                return head;
            }
            ListNode * newhead = reverseList(head->next);
            //newhead是为了将新的头节点地址逐层带回到最外层递归函数作为返回值
            head->next->next = head;
            //从原尾结点开始实现反向链接
            head->next = nullptr;
            //这里逐层置空是为了最后将新的尾结点指针域置空
            return newhead;           
        }

由于递归算法开销比较大,所以题解接口中我们采用三指针反转法来完成链表的反转

(3) 双指针法判断链表是否回文

经过前两个步骤后,链表后半部分完成了反转:数据结构:链表基础OJ练习+带头双向循环链表的实现

最后在题解接口中用双指针判断链表是否回文即可

数据结构:链表基础OJ练习+带头双向循环链表的实现

题解代码: 

class Solution 
{
public:
    ListNode * FindMid(ListNode * head)   //快慢指针法寻找链表中间位置节点的接口
    {
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast && fast->next)
        {
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;                       //返回链表中间位置节点
    }
    ListNode * reverse (ListNode * head)   //反转链表的接口(三指针翻转法)
    {
        ListNode * cur = (nullptr == head)? nullptr : head->next;
        ListNode * pre = head;
        while(cur)
        {
            ListNode* Next = cur->next;
            cur->next = pre;
            pre = cur;
            cur = Next;
        }
        if(head)       
        head->next = nullptr;   //记得将被反转部分链表的尾结点的指针域置空
        return pre;             //pre最终指向反转链表的表头
    }
    bool isPalindrome(ListNode* head) 
    {
        ListNode* mid = FindMid(head);
        ListNode * reversehead = reverse(mid);
        ListNode * tem = reversehead;
        while(reversehead)
        {
            if(reversehead->val != head->val)
            {
                return false;
            }
            reversehead = reversehead->next;
            head = head->next;
        }
        reverse(tem);             //恢复原链表
        return true;
    }
};

数据结构:链表基础OJ练习+带头双向循环链表的实现

二.带头双向循环链表的实现

链表共有8个种类,然而在现实中大多情形下能派上用场的链表只有两种:

  1. 无头单向非循环链表:实际中无头单向非循环链表常作为其他数据结构的子结
    ,如哈希桶、图的邻接表等等

    数据结构:链表基础OJ练习+带头双向循环链表的实现
  2. 带头双向循环链表:该种链表结构是由设计C++STL的大神设计出来的,结构优良,使用和实现起来都比较方便(每个节点都有两个指针域,比较耗空间)
    数据结构:链表基础OJ练习+带头双向循环链表的实现

每个带头双向循环链表都有一个哨兵头节点,该节点不存储有效数据.

带头双向循环链表的环状示意图:

数据结构:链表基础OJ练习+带头双向循环链表的实现

1.头文件

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int LTDataType;

typedef struct LTNode
{
    LTDataType val;
    struct LTNode* pre;           //指向前一个节点的指针
    struct LTNode* next;          //指向下一个节点的指针
}LTNode;


//各个链表操作接口的声明
LTNode* BuyLTNode(LTDataType x);
void ListPrint(LTNode* phead);
LTNode* ListInit();
LTNode* ListFind(LTNode* phead, LTDataType x);

void ListInsert(LTNode* pos, LTDataType x);
void ListErase(LTNode* pos, LTNode* phead);
void ListPushFront(LTNode* phead, LTDataType x);
void ListPopFront(LTNode* phead);
void ListPopBack(LTNode* phead);
void ListPushBack(LTNode* phead, LTDataType x);
void ListDestory(LTNode* phead);

2.节点内存申请接口和链表初始化接口

节点内存申请接口:

LTNode* BuyLTNode(LTDataType x)  //向系统申请链表节点空间的接口
{
    LTNode* NewNode = (LTNode*)malloc(sizeof(LTNode));
    if (NULL == NewNode)
    {
        perror("malloc failed:");
        exit(-1);
    }
    NewNode->next = NULL;
    NewNode->pre = NULL;
    NewNode->val = x;
    return NewNode;
}

链表初始化接口:

LTNode* ListInit()    //链表初始化接口(链表初始化时创建哨兵节点,接口返回哨兵节点的地址)
{
    LTNode* phead = BuyLTNode(-1);
    phead->next = phead;
    phead->pre = phead;
    return phead;
}

带头双向循环链表的初始化就是申请一个哨兵节点:数据结构:链表基础OJ练习+带头双向循环链表的实现

使用时在主函数中用一个LTNode类型的指针接收该哨兵节点的地址.比如:

int main ()
{
    phead = ListInit();

    // 其他链表操作

    return 0;
}

3.链表的打印和查找接口

带头双向循环链表的遍历过程是从哨兵节点的下一个结点开始哨兵节点前一个节点结束。

数据结构:链表基础OJ练习+带头双向循环链表的实现

void ListPrint(LTNode* phead)     //打印链表接口(注意不要打印哨兵节点中的无效数据)
{
    assert(phead);
    LTNode* tem = phead->next;
    while (tem != phead)
    {
        printf("%d ", tem->val);
        tem = tem->next;
    }
    printf("\n");
}
LTNode* ListFind(LTNode* phead, LTDataType x)  //根据节点中存储的数据查找某个链表节点
{
    assert(phead);
    LTNode* tem = phead->next;
    while (tem != phead)
    {
        if (x == tem->val)
        {
            return tem;
        }
        tem = tem->next;
    }
    return NULL;
}

4.链表的增删接口

  • 删除pos地址处节点的接口数据结构:链表基础OJ练习+带头双向循环链表的实现 
void ListErase(LTNode* pos, LTNode* phead)    //删除pos位置的节点
{
    assert(pos && pos != phead);
    LTNode* Pre = pos->pre;
    LTNode* Next = pos->next;
    Pre->next = Next;
    Next->pre = Pre;
    free(pos);
    pos = NULL;
}
  • 在pos地址节点的后一个位置插入一个节点的接口:数据结构:链表基础OJ练习+带头双向循环链表的实现
void ListInsert(LTNode* pos, LTDataType x)    //在pos位置后插入一个链表节点的接口
{
    assert(pos);
    LTNode* newnode = BuyLTNode(x);
    LTNode* Next = pos->next;

    pos->next = newnode;
    newnode->pre = pos;
    newnode->next = Next;
    Next->pre = newnode;
}

头插,头删以及尾插尾删接口通过复用上面的两个接口即可实现

void ListPushFront(LTNode* phead, LTDataType x) //头插一个节点
{
    assert(phead);
    ListInsert(phead, x);
}
void ListPopFront(LTNode* phead)                //头删一个节点
{
    assert(phead && phead->next != phead);
    ListErase(phead->next, phead);
}
void ListPopBack(LTNode* phead)                 //尾删一个节点
{
    assert(phead && phead->pre != phead);
    ListErase(phead->pre, phead);
}
void ListPushBack(LTNode* phead, LTDataType x)  //尾插一个节点
{
    assert(phead);
    ListInsert(phead->pre, x);
}
  • 注意哨兵节点不允许在删除数据操作中被删除

5.链表销毁接口

void ListDestory(LTNode* phead)                 //销毁链表的接口
{
    assert(phead);
    LTNode* tem = phead->next;
    while (tem != phead)
    {
        LTNode* Next = tem->next;
        free(tem);
        tem = Next;
    }
    free(phead);
    phead = NULL;
}
  •  注意哨兵节点最后才销毁

可以看见,带头双向循环链表各个操作接口的时间复杂度都是O(1),这点充分体现了其数据结构的优良性。 

 数据结构:链表基础OJ练习+带头双向循环链表的实现文章来源地址https://www.toymoban.com/news/detail-434561.html

到了这里,关于数据结构:链表基础OJ练习+带头双向循环链表的实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【数据结构】链表:带头双向循环链表的增删查改

    本篇要分享的内容是带头双向链表,以下为本片目录 目录 一、链表的所有结构 二、带头双向链表 2.1尾部插入 2.2哨兵位的初始化 2.3头部插入 2.4 打印链表 2.5尾部删除 2.6头部删除  2.7查找结点 2.8任意位置插入 2.9任意位置删除  在刚开始接触链表的时候,我们所学仅仅所学的

    2024年02月05日
    浏览(78)
  • 【数据结构】—带头双向循环链表的实现(完美链表)

    链表结构一共有八种形式,在前面的文章里已经讲完了不带头单向非循环链表的实现,但是我们发现该链表实现尾插与尾删时比较麻烦,要先从头节点进行遍历,找到尾节点,时间复杂度为O(N),而本次所讲的带头双向循环单链表,则可以直接找到尾节点。 虽然该链表看起来

    2024年01月25日
    浏览(48)
  • 【数据结构初阶】四、线性表里的链表(带头+双向+循环 链表 -- C语言实现)

    ========================================================================= 相关代码gitee自取 : C语言学习日记: 加油努力 (gitee.com)  ========================================================================= 接上期 : 【数据结构初阶】三、 线性表里的链表(无头+单向+非循环链表 -- C语言实现)-CSDN博客  ====

    2024年02月08日
    浏览(35)
  • 【数据结构和算法】实现带头双向循环链表(最复杂的链表)

    前文,我们实现了认识了链表这一结构,并实现了无头单向非循环链表,接下来我们实现另一种常用的链表结构,带头双向循环链表。如有仍不了解单向链表的,请看这一篇文章(7条消息) 【数据结构和算法】认识线性表中的链表,并实现单向链表_小王学代码的博客-CSDN博客

    2024年01月17日
    浏览(64)
  • 数据结构:带环单链表基础OJ练习笔记(leetcode142. 环形链表 II)(leetcode三题大串烧)

    目录 一.前言  二.leetcode160. 相交链表  1.问题描述 2.问题分析与求解 三.leetcode141. 环形链表 1.问题描述 2.代码思路  3.证明分析  下一题会用到的重要小结论: 四.leetcode142. 环形链表 II 1.问题描述 2.问题分析与求解 Judgecycle接口: 方法一: 方法二:  单链表和带环单链表

    2023年04月08日
    浏览(30)
  • 链接未来:深入理解链表数据结构(二.c语言实现带头双向循环链表)

    上篇文章简述讲解了链表的基本概念并且实现了无头单向不循环链表:链接未来:深入理解链表数据结构(一.c语言实现无头单向非循环链表)-CSDN博客 那今天接着给大家带来带头双向循环链表的实现 : 头文件DoubleList.h:用来基础准备(常量定义,typedef),链表表的基本框架

    2024年01月16日
    浏览(51)
  • 数据结构课程设计题目——链表综合算法设计、带头双向循环链表、插入、显示、删除、修改、排序

      课程设计题目1–链表综合算法设计   一、设计内容   已知简单的人事信息系统中职工记录包含职工编号(no)、职工姓名(name)、部门名称(depname)、职称(title)和工资数(salary)等信息(可以增加其他信息),设计并完成一个简单的人事信息管理系统,要求完成但不

    2024年02月08日
    浏览(52)
  • 数据结构——带头双向循环链表

    在创建带头双向循环链表的节点中比之前单链表节点的创建多了一个struct ListNode* prev;结构体指针,目的在与存储前一个节点的地址,便于将整个链表连在一起。 动态申请内存结点,函数返回的是一个指针类型,用malloc开辟一个LTNode大小的空间,并用node指向这个空间,再判断

    2024年02月09日
    浏览(29)
  • 【数据结构】带头双向循环链表

      🧑‍🎓 个人主页:简 料   🏆 所属专栏:C++   🏆 个人社区:越努力越幸运社区   🏆 简       介: 简料简料,简单有料~在校大学生一枚,专注C/C++/GO的干货分享,立志成为您的好帮手 ~ C/C++学习路线 (点击解锁) ❤️ C语言阶段(已结束) ❤️ 数据结构与算法(ing) ❤

    2024年01月16日
    浏览(69)
  • 数据结构---带头双向循环链表

    什么是双向带头循环链表? 上面简单的一个非空 带头循环双向链表逻辑图 如何定义一个双向链表? 根据图和代码可以看双向链表就是单链表的每个结点中,在设置一个指向前驱节点的指针 简单认识之后,对他进行初始化(申请一个头节点,让前驱和后驱指针都指向自己) 代码

    2024年02月07日
    浏览(53)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包