【数据结构初阶】单链表补充内容+又双叒叕刷链表题

简介: 【数据结构初阶】单链表补充内容+又双叒叕刷链表题

1.顺序表&双向循环链表的优点和缺点


顺序表:


一.优点:


尾插尾删效率很高

支持用下标随机访问

二.缺点:


头部和中部插入和删除效率低O(n)

扩容-----性能消耗+空间消耗

双向循环链表:


一.优点:


任意位置插入删除效率很高O(1)

按需申请释放

二.缺点:


不支持随机访问

综合而言,两个各有优缺,相辅相成,具体用谁看场景,但是总体而言顺序表使用的频率更高一点


扩展一点:


顺序表的cpu高速缓存命中率高(顺序表空间连续)


633ea908e4ba497ab81c08812aa5ebb6.png

2.单链表和双向循环链表


1.单链表:其他结构的子结构(比如哈希桶),面试经常问到和OJ题目,且只适合头部的插入删除。

2.双向循环链表:结构复杂,但是实现简单,最为实用,常被用于实际存数据,适合任意位置的插入删除。


3.一点杂七杂八的东西

3-1顺序表和链表打印的断言

1.顺序表和链表打印断言
顺序表定义:SeqList Sq;
在传给打印函数的时候:SeqListPrint(&Sq)
在函数内部的时候&Sq一定不为空,void SeqListPrint(SeqList* ps)所以要断言一下assert(ps);
单链表定义变量: SLTNode* phead;
在传给打印函数的时候:SLTNodePrint(&phead);
在函数内部打印的时候phead为空的时候,&phead也可能为空,所以不能断言


但是注意:虽然phead为空,但是pphead不为空 ,所以在链表头插函数内部还是要对pphead断言。

原因:phead没有指向任何内容,但是phead本身是一个指针变量,是变量就有地址。

ad253b108a9346d59e4bfdd79e3c723c.png

85b86a453a5944b0ad96c582156ebd0f.png


3-2.栈上和堆上定义一个新节点

2.栈(错)和堆上(对)定义一个新节点
void SLTNodePushFront(SLTNode** pphead,STDateType x)
{
   //错误示范:栈上开辟的,出函数销毁
   SLTNode newnode;
   //正确做法:malloc结点,堆上开辟,空间需要手动释放
   SLTNode* newhead=(SLTNode*)malloc(sizeof(SLTNode));
}

3-3.二级指针


我们在主函数定义了SListNode*  plist;


在改变plist的指向的时候就要传二级指针,但是如果在函数内改结点,直接用一级指针就能改,就像我们在函数内部定义的prev,cur等。这也就是哨兵位头结点的原理,在因为有哨兵位的头结点,就不用改plist,所以可以在主函数内传一级指针。

3.二级指针
以尾插函数为例,如果主函数里的plist为空,那么要改plist就要传二级指针
但是为什么在plist不为空的时候,要尾插一个
void SLTNodePushBack(SLTNode* pphead, SLTDateType x)//1
{
  assert(pphead);//2
  SLTNode* newhead = (SLTNode*)malloc(sizeof(SLTNode));
  newnode->next = NULL;
  newnode->date = x;
  if (*pphead == NULL)
  {
    *pphead = newnode;
  }
  else
  {
    SLTNode* tail = *pphead;
    while (tail)
    {
      tail = tail->next;
    }
    tail->next = newnode;//3
  }
}

无头单向不循环链表传二级指针发挥作用的情况:

  • 没有结点尾插
  • 只有一个结点尾删
  • 头插
  • 头删
  • 销毁

3-4.哨兵头结点的作用

头结点的作用:

  1. 方便对plist为空等特殊情况时的统一操作
  2. 避免传二级指针

备注:有人说哨兵位头结点的数据域是用来存储单链表的长度;

专业打假:其实这种说法是错误的,因为结点的数据域为char类型的且链表长度大于127的时候就会溢出,所以这种说法是错误的。

3-5 为什么单链表的基本操作中无tail记录尾

看过我写题的应该知道,在建立新链表的时候,通常是采用尾插操作,尾插相对于头插能够保证新链表的结点在原链表的相对顺序不变,但是尾插的缺点是每次尾插都要找尾,所以我们定义一个尾指针,实时更新尾指针。


那为什么单链表的基本操作中无tail记录尾?那是因为在基本操作中不止有尾插,还有尾删,定义一个tail效果适用性不是很强。

4.替换法删除pos结点


image.png

基于单链表删除pos位置还要找尾的麻烦,删除pos位置的结点有一个替换法删除,数据域值交换的方法,时间复杂度是O(1):


如果要删除pos位置的结点,则可以狸猫换太子把pos后面一个结点的值给pos位置的数据域,然后pos->next=pos->next->next,也就是把pos后面那个结点作替死鬼。


缺点:pos位置不能是尾结点。


pos位置是尾结点且链表是循环链表就可以,但是如果是循环链表的话就没必要使用替换法删除pos位置的结点.


4-1.变式

如果要在pos位置之前插入一个结点,时间复杂度为O(1),也可以采用这种方法:

BuySListNode(pos->val);

pos->val=x;

5 .链表调试

当OJ写不出来,想在VS上调试代码,找Bug,但是每次还要重建链表?有什么解决办法?

备注:程序员一半的时间都在改Bug,你连调试都不会,就等着扣绩效吧😁😁

解决办法:在桌面上备份一份单链表的代码,方便OJ调试。(代码如下

#define _CRT_SECURE_NO_WARNINGS 1
struct  ListNode
{
  int val;
  struct ListNode* next;
};
#include<stdio.h>
#include<stdlib.h>
int main()
{
  struct  ListNode* n1 = (struct  ListNode*)malloc(sizeof(struct  ListNode)); 
  struct  ListNode* n2 = (struct  ListNode*)malloc(sizeof(struct  ListNode));
  struct  ListNode* n3 = (struct  ListNode*)malloc(sizeof(struct  ListNode));
  struct  ListNode* n4 = (struct  ListNode*)malloc(sizeof(struct  ListNode));
  struct  ListNode* n5 = (struct  ListNode*)malloc(sizeof(struct  ListNode));
  struct  ListNode* n6 = (struct  ListNode*)malloc(sizeof(struct  ListNode));
  struct  ListNode* n7 = (struct  ListNode*)malloc(sizeof(struct  ListNode));
  n1->val = 1;
  n2->val = 2;
  n3->val = 3;
  n4->val = 4;
  n5->val = 5;
  n6->val = 6;
  n7->val = 7;
  n1->next = n2;
  n2->next = n3;
  n3->next = n4;
  n4->next = n5;
  n5->next = n6;
  n6->next = n7;
  n7->next = NULL;
  return 0;
}

6.为什么学校老师讲的时候不先讲带头结点的

  1. 实际应用中很少带头
  2. OJ题的head大大部分都是不带头的


7.刷刷刷题

上次的链表题刷的过瘾吗?链表习题集1

我又双叒叕来了,记住题目是环环相扣的,记得先把习题集1做了哦,有些解法这里用到将不会再细讲...,敬请谅解


7-1.回文链表

OR36 链表的回文结构


0dbbe1e83c6351d42c829e739275a1ed.png

862e42878379492e9597f24cb50799f1.png

思路1:

第一步:利用快慢指针找到链表的中间结点slow

第二步:将中间结点开始以后的结点部分逆置(反转),得到相交链表的新头指针prev

第三步:遍历判断两个链表是否值相等

反转过程是最重要的部分,这里给出一点图理解一下


a3a2f276a0b2403ea5965cc2f8a78756.png


class PalindromeList {
public:
    bool chkPalindrome(ListNode* phead) {
        // 第一步:快慢指针求中间结点slow
        struct ListNode* fast,*slow;
        fast=slow=phead;
        while(fast&&fast->next)
        {
            fast=fast->next->next;
            slow=slow->next;
        }
        //第二步:部分反转,得新头结点prev
        struct ListNode* prev=NULL;
        struct ListNode* cur=slow;
        while(cur)
        {
            struct ListNode* next=cur->next;
            cur->next=prev;
            prev=cur;
            cur=next;
        }
        //第三步:判断新旧链表是否相等
        struct ListNode* newhead=prev;
        struct ListNode* oldhead=phead;
        while(newhead)
        {
            if(newhead->val!=oldhead->val)
            {
                return false;
            }
            newhead=newhead->next;
            oldhead=oldhead->next;
        }
        return true;
    }
};

这里其实形参名是可以改成自己喜欢的名字,而不是题目这么挫的A


思路2:牢牢抓住回文链表的定义,从前往后和从后往前读都是一样的


第一步:拷贝原链表,得到头指针copyhead

第二步:将原链表整体反转,得到r头指针eversehead

第三步:遍历判断两个链表是否值相等

(备注:这里一定一定要记得先拷贝一份原链表,否则反转后得到的链表将不再是原先那个链表!)

class PalindromeList {
public:
    //动态开辟一个新节点,新节点的值为cur->val
    struct ListNode* BuySListNode(int val)
    {
        struct ListNode* newnode=(struct ListNode*)malloc(sizeof(struct ListNode));
        newnode->val=val;
        newnode->next=NULL;
        return newnode;
    }
    bool chkPalindrome(ListNode* phead) {
        //第一步:拷贝原链表,得到copyhead
        struct ListNode* Guard,*Tail;
        Guard=Tail=(struct ListNode*)malloc(sizeof(struct ListNode));
        struct ListNode* cur=phead;
        while(cur)
        {
            struct ListNode* next=cur->next;
            struct ListNode* newnode=BuySListNode(cur->val);
            Tail->next=newnode;
            Tail=Tail->next;
            cur=next;
        }
        struct ListNode* copyhead=Guard->next;
        free(Guard);
        Guard=NULL;
        //第二步:将原链表整体反转,得到reversehead
        struct ListNode* prev=NULL;
        struct ListNode* cur2=phead;
        while(cur2)
        {
            struct ListNode* next=cur2->next;
            cur2->next=prev;
            prev=cur2;
            cur2=next;
        }
        struct ListNode* reversehead=prev;
        //第三步:遍历判断两个链表是否值相等
        while(copyhead)
        {
            if(copyhead->val!=reversehead->val)
            {
                return false;
            }
            copyhead=copyhead->next;
            reversehead=reversehead->next;
        }
        return true;
    }
};

7-2.相交链表

力扣之相交链表b285b2a2b6604f4ba4f63ad51a1f1a81.png

题目的相交链表可能有的同学会误以为是X字形,但是却只有Y字形,原因是每一个结点只有一个指针域,没有两个指针域。


f8bff1c2550d41778a9f522500a9f732.png


思路:


第一步:分别遍历两个链表,分别求出两个链表的长度

第二步:让长度长的那个链表先走两链表长度的差距步

第三步:短链表从头开始走,长链表从差距步开始同步走

第四步:相遇点即是相交链表的交点结点,返回


struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    //第一步:分别遍历两个链表,分别求出两个链表的长度
    int lenA=1,lenB=1;
    struct ListNode* TailA=headA;
    struct ListNode* TailB=headB;
    while(TailA->next)
    {
        ++lenA;
        TailA=TailA->next;
    }
    while(TailB->next)
    {
        ++lenB;
        TailB=TailB->next;
    }
    if(TailA!=TailB)
    {
        return false;
    }
    //第二步:让长度长的那个链表先走两链表长度的差距步
    struct ListNode* longhead=headA;
    struct ListNode* shorthead=headB;
    if(lenA<lenB)
    {
        longhead=headB;
        shorthead=headA;
    }
    int div=abs(lenA-lenB);
    struct ListNode* cur1=longhead;
    struct ListNode* cur2=shorthead;
    while(div--)
    {
        cur1=cur1->next;
    }
    //第三步:短链表从头开始走,长链表从差距步开始同步走
    while(cur1!=cur2)
    {
        cur1=cur1->next;
        cur2=cur2->next;
    }
    //第四步:相遇点即是相交链表的交点结点,返回
    return cur1;
}

7-3. 链表分割

链表分割

5f6051e7805e46caa92308a4331e6571.png

思路:将链表结点按数据值<x和>=x尾插到相应链表 ,然后再将两分割的链表连接起来。

第一步:定义大小Guard和Tail指针

第二步:   遍历尾插到相应新链表

第三步: 连接两个分割的链表

备注:记得将bigTail->next置空!!!否则会运行超时


35646a8ba8844072a936925727874374.png

class Partition {
public:
    ListNode* partition(ListNode* pHead, int val) {
        // write code here
        //第一步:定义大小Guard和Tail指针
        struct ListNode* lessGuard,*lessTail;
        lessGuard=lessTail=(struct ListNode*)malloc(sizeof(struct ListNode));
        lessGuard->next=NULL;
        struct ListNode* bigGuard,*bigTail;
        bigGuard=bigTail=(struct ListNode*)malloc(sizeof(struct ListNode));
        bigGuard->next=NULL;
        //第二步:   遍历尾插到相应新链表
        struct ListNode* cur=pHead;
        while(cur)
        {
            struct ListNode* next=cur->next;
            if(cur->val<val)
            {
                lessTail->next=cur;
                lessTail=lessTail->next;
            }
            else 
            {
                bigTail->next=cur;
                bigTail=bigTail->next;
            }
            cur=next;
        }
        //第三步: 连接两个分割的链表
        lessTail->next=bigGuard->next;
        bigTail->next=NULL;
        struct ListNode* newhead=lessGuard->next;
        free(lessGuard);
        free(bigGuard);
        lessGuard=NULL;
        bigGuard=NULL;
        return newhead;
    }
};
目录
相关文章
|
20天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
48 4
|
21天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
21天前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
1月前
|
存储
[数据结构] -- 单链表
[数据结构] -- 单链表
26 1
|
20天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
39 0
|
1月前
|
存储
[数据结构] -- 双向循环链表
[数据结构] -- 双向循环链表
23 0
|
1月前
|
存储
数据结构(单链表)
数据结构(单链表)
15 0
|
1月前
|
存储
探索数据结构:便捷的双向链表
探索数据结构:便捷的双向链表
|
18天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
94 9
|
9天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
19 1

热门文章

最新文章

下一篇
无影云桌面