想入门数据结构,却总是拜倒在链表的石榴裙下?

大家好,我是melo,一名大二上软件工程在读生,经历了一年的摸滚,现在已经在工作室里边准备开发后台项目啦
不过这篇文章呢,还是想跟大家聊一聊数据结构与算法,学校也是大二上才开设了数据结构这门课,希望可以一边学习数据结构一边积累后台项目开发经验

写在前边

相信很多小猿人在初入数据结构的时候,或者说是在学习c语言的后期时分,总会遇到一个馋(缠)人的绕来绕去的家伙–就是我们今天要讲的链表。为什么说链表缠人呢,链表的分类,题型的多样性,链表的用途等等都是很多的,限于篇幅本篇着重来讨论一下学习链表以及做题时的思路概览,注意点以及有哪些 小技巧
另外,本博客题解一般会使用c(学校课内要求)和java(个人后台方向需要熟练应用)两种语言

ps:本博客旨在提供一些学习和做题时的小技巧,推荐搭配数据结构算法书籍一同学习和补充知识点
入门的话比较推荐的有《啊哈!算法》,《大话数据结构》,这两本书都偏向图文式教学,也有很多可可爱爱的插画

想入门数据结构,却总是拜倒在链表的石榴裙下?

小哈确实好可爱啊哈哈哈

  • 关于链表的定义,优点相信你们也都在书中看了蛮多了,这里就不多加阐述,我们直接来看形形色色的各种链表,看他们的区别和实现

链表的分类

我们先来看看最简单的单向链表

  • 链表是有序的列表,但是它在内存中是这样存储的

想入门数据结构,却总是拜倒在链表的石榴裙下?
1)链表是以节点的方式来存储,是链式存储
2)每个节点包含data域,next域:指向下一个节点.

3)如图:发现链表的各个节点不一定是连续存储.

4)链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定

带头结点逻辑结构示意图如下

head中不存储实际的数据data,只是用next来指向真正带有数据的第一个节点

想入门数据结构,却总是拜倒在链表的石榴裙下?

不带头结点

第一个节点即存储有实际数据data和next的实际节点

何时要头结点何时不需要呢

  • 有无头结点的区别在于,链表的第一个节点是否存储值。那我们可以先大概下个定义:如果说我们的操作 会影响到第一个节点的(比如说删除链表中的某个值),那是不是我们尽量就用 带头结点的会好一点

例如:删除结点时,如果我们不带头结点的话,如果删除的是第一个实际节点,那我们还需要去更改头结点,让头结点指向第二个实际节点(因为第一个实际节点已经被删除了,自然就不是头结点了)
而带有头结点的话,恰好就能解决这种需要特判的情况

这个区别,还需要我们慢慢去体会,在下文讲到链表删除的时候自然而然就会更加深刻体会到了,这里只是先点一下,而到了后边环形链表的时候,更多的是没带头结点的版本

Typedef(偷懒神器)

  • PNODE就等价于 struct Node* 了!

想入门数据结构,却总是拜倒在链表的石榴裙下?
想入门数据结构,却总是拜倒在链表的石榴裙下?

LinkList就直接等价于struct LNode*,可以省去很多书写,下文中也会体现到

创建链表

思路概览

  • 我们需要两个结点,一个指向当前节点,一个指向我们新创建的节点,每次创建新节点后,让当前节点指向新节点,并移动当前节点到新节点上

记得最后跳出循环的时候,要让当前节点指向null来结束链表!

流程

链表节点定义(typedef妙用,后续可以省去struct)

#include
#include

typedef struct ListNode {
    int val;
    struct ListNode* next;
};

注意c语言需要修改头文件中的cstido为stdio.h

注意让当前pNow=head

可以省去特判i=0

//根据读取的元素创建链表
ListNode* creatLinkedList() {
    ListNode* head = (ListNode*)malloc(sizeof(ListNode));
    ListNode* pNow = head;
    int n,temp;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        ListNode* pNew = (ListNode*)malloc(sizeof(ListNode));
        scanf("%d", &(pNew->val));
        //若是第一个
        /*if (i == 0) {
            head->next = pNew;
        }*/
        //将当前节点指向新节点,并移动当前节点到新节点上
        pNow->next = pNew;
        pNow = pNew;
    }
    //跳出循环后,使得当前节点指向null终止链表
    pNow->next = NULL;
    return head;
}

特判第一个多一步 head->next=pNew

其实可以不用,在不带头结点的链表创建中,才需要去特判头结点

记住最后跳出循环让pNow->next=null

遍历链表

  • 注意让p=head->next,然后遍历p(p=p->next)判断 p !=null 即可
//遍历链表
void traverseLinkedList(ListNode* head) {
    for (ListNode* p = head->next; p != NULL; p=p->next) {
        printf("%d ", p->val);
    }
}

链表插入

在指定位置插入

想入门数据结构,却总是拜倒在链表的石榴裙下?

思路概览

  • 首先我们第一感觉就是邀先移动到指定位置的前一位p,然后去新开辟一个节点pNew,让p指向pNew,pNew指向原来p的下一节点

问题

  • 如果我们原原本本按照思路来,有没有发现,其实让pNew指向原来p的下一节点,但是此时p的下一节点是什么呢,其实已经是pNew了

所以我们应该先用另一个指针,存储一下原来p都下一个节点
或者说注意一下顺序,先让pNew的next=p的next
然后再让p都next=pNew

注意

**校验合法性

  • 用while 和if 两个条件刚好相反 可以 *省去获取链表长度的操作
//链表插入(三种情况都综合成一种)
bool insertLinkedList(ListNode* head, int position, int data) {
    int i = 0;
    ListNode* p = head;
    ListNode* pNew = (ListNode*)malloc(sizeof(ListNode));
    //移动p直到目标位置的前一位,条件是p不为null
    while (i < position - 1 && p!=NULL) {
        i++;
        p = p->next;
    }
    //若移动不到,返回false
    if (i != position - 1 || p==NULL) {
        return false;
    }
    pNew->val = data;
    //注意顺序
    //先让新节点指向上一个节点的下一节点
    //然后再让上一个节点指向新节点
    pNew->next = p->next;
    p->next = pNew;
    return true;
}

链表删除

(力扣)203移除链表元素(删除所有满足指定值的节点)

思路概览

  • 我们需要定义两个指针,一个last记录前一个节点,一个记录当前节点now,怎么删除呢?让 last(图中的p)->next=now(图中的q)->next就可以了

想入门数据结构,却总是拜倒在链表的石榴裙下?

***递归还未看

https://leetcode-cn.com/problems/remove-linked-list-elements/

想入门数据结构,却总是拜倒在链表的石榴裙下?

特判头指针法

struct ListNode* removeElements(struct ListNode* head, int val){
    //前指针
    struct ListNode* last=head;
    //当前指针
    struct ListNode* now=head;
    //now肯定要移动,而last就不一定要移动了!!!(此处注意)
    for(;now!=NULL;now=now->next){
        if(now->val==val){
            //特判删除头节点
            if(now==head){
                head=now->next;
            }
            //一般情况
            else{
                last->next=now->next;
            }
        }
        //不删,则移动last,若删了就不用移动!!!

        else{
            last=now;
        }
    }
    return head;
}

注意last需不需要移动!!!,且last默认先给头节点

虚拟头结点法(注意最后return)

struct ListNode_ dummyHead= (struct ListNode_)malloc(sizeof(struct ListNode));

struct ListNode* removeElements(struct ListNode* head, int val){
    //创建虚拟头节点,并指向第一个节点
struct ListNode* dummyHead= (struct ListNode*)malloc(sizeof(struct ListNode));
    dummyHead->next=head;
    //前指针
    struct ListNode* last=dummyHead;
    //当前指针
    struct ListNode* now=head;
    //now肯定要移动,而last就不一定要移动了!!!(此处注意)
    for(;now!=NULL;now=now->next){
        if(now->val==val){
            //特判删除头节点
            last->next=now->next;
        }
        //不删,则移动last,若删了就不用移动!!!

        else{
            last=now;
        }
    }
    return dummyHead->next;
}

(最优)while+虚拟头

delete temp是c++中的,此处java会自动回收无需释放空间

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        //创建虚拟头结点
        ListNode dummyHead = new ListNode(0);
        //让虚拟头结点指向给定的head结点
        dummyHead.next = head;
        //定义一个临时结点来遍历,默认从虚拟头结点开始
        ListNode temp = dummyHead;
        while (temp.next != null) {
            if (temp.next.val == val) {
                //找到了则删除
                temp.next = temp.next.next;
            } else {
                //找不到继续移动
                temp = temp.next;
            }
        }
        return dummyHead.next;
    }
}

**移除链表元素(指定位置)

题目

想入门数据结构,却总是拜倒在链表的石榴裙下?

题解

#include "allinclude.h"  //DO NOT edit this line
Status Delete_L(LinkList L, int i, ElemType &e)
{   // Add your code here
  LNode* p = L;
  int j=0;
  //注意跟新增一个节点不一样,此处得p->next!=NULL
  while(jnext!=NULL){
    p=p->next;
    j++;
  }
  //若参数不合法
  if(j!=i-1||p->next==NULL){
    return ERROR;
  }
  e= (p->next)->data;
  //LNode* temp = p->next;
  p->next = p->next->next;
  //free(temp);
  return OK;

}

**注意

  • 跟新增一个节点不一样,此处得p->next!=NULL,而不是p!=NULL

若是p!=NULL的话,下边我们又要保存p->next, 可能就非法访问了!

*从第i元素起的所有元素从链表移除

题目

想入门数据结构,却总是拜倒在链表的石榴裙下?

还记得上文的typedef吗,此处定义了一个LinkList就等价于LNode
相当于Typedef struct LNode LinkList

题解

#include "allinclude.h"  //DO NOT edit this line
Status Split_L(LinkList L, LinkList &Li, int i)
{   // Add your code here
  int cnt=0;
  LNode* p = L ;
  LinkList temp;
  //移动到前一位
  while(cntnext!=NULL){
    p=p->next;
    cnt++;
  }
  //校验参数合法性
  if(cnt!=i-1||p->next==NULL){
    Li=NULL;
    return ERROR;
  }
  temp=(LinkList)malloc(sizeof(LNode));
  if(temp==NULL) return ERROR;
  Li=temp;
  Li->next=p->next;
  //销毁i元素后
  p->next=NULL;
  return OK;
}

链表反转

(力扣)206反转链表

https://leetcode-cn.com/problems/reverse-linked-list/

想入门数据结构,却总是拜倒在链表的石榴裙下?

思路概览

  • 让后一个指向前一个,同时要记得要用另外一个指针 保存下一个节点位置用于正常遍历,总共需要三个指针

官方题解(迭代+三指针)

c语言

/**
 * Definition for singly-linked list.

 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

struct ListNode* reverseList(struct ListNode* head){
    struct ListNode* prev = NULL;
    struct ListNode* curr = head;
    while (curr) {
        //先记录curr的下一位,后续才能修改curr的下一位(总结就是要修改哪个值,就先保存下来)
        struct ListNode* next = curr->next;
        curr->next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}

java版

区别就在于java没有指针而是引用,直接ListNode就好,没有那个*(写法方便一些)

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr) {
            ListNode next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }
}

要改变什么值,就先用一个临时值来存储

链表反转中,要改变next,又怕影响 正常的遍历,所以先用一个临时指针来存储就好了,这样就还能正常的遍历下去

合并两个有序链表

(力扣)21合并两个有序链表

https://leetcode-cn.com/problems/merge-two-sorted-lists/

想入门数据结构,却总是拜倒在链表的石榴裙下?

思路

  • 同时遍历两个链表l1和l2,直到有一个为null
  • 每次遍历时,判断是哪个比较小,然后加到新链表中,移动l1指针

直接if单独比较版本

一次只能一个,但是就不用嵌套while(while还要再次判断是不是空)

struct ListNode* mergeTwoLists2(struct ListNode* l1, struct ListNode* l2) {
    struct ListNode* drummyHead = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* Node = drummyHead;
    while (l1 != NULL && l2 != NULL) {
        if( l1->val val) {
            Node->next = l1;
            l1 = l1->next;
        }
        else{
            Node->next = l2;
            l2 = l2->next;
        }
        Node = Node->next;
    }
    //最后出循环必有一个为null(而且不会同时为null)则让非空的剩余部分连上去即可
    Node->next = l1 == NULL ? l2 : l1;
    /*ListNode* p = drummyHead->next;
    while (p) {
        printf("%d", p->val);
        p = p->next;
    }*/
    return drummyHead->next;
}

while中嵌套while版本

注意

内部while可能会破坏外部大while的条件,需要再次判断

struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2) {
    struct ListNode* drummyHead = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* Node = drummyHead;
    while (l1 != NULL && l2 != NULL) {
        while (l1 != NULL && l2 != NULL && l1->val val) {
            Node->next = l1;
            Node = Node->next;
            l1 = l1->next;
        }
        //可能l1已经为NULL了,所以也还得判断l1
        while (l2 != NULL && l1 != NULL && l2->val val) {
            Node->next = l2;
            Node = Node->next;
            l2 = l2->next;
        }
    }
    //最后出循环必有一个为null(而且不会同时为null)则让非空的剩余部分连上去即可
    Node->next = l1 == NULL ? l2 : l1;
    /*ListNode* p = drummyHead->next;
    while (p) {
        printf("%d", p->val);
        p = p->next;
    }*/
    return drummyHead->next;
}

总结

巧用typedef偷懒神器

有时候题目给的是不带头结点的,尽量自己去构造一个虚拟头指针,可以省去一些特判的操作

要改变什么值,就先用一个临时值来存储

链表反转中,要改变next,又怕影响 正常的遍历,所以先用一个临时指针来存储就好了,这样就还能正常的遍历下去

最后

  • 有关其他链表知识,环形链表,约瑟夫等经典问题,melo最近比较忙,忙于学习 多线程知识接管一下项目,等到以后学校课程(小声bb学校刚讲到创建和遍历链表)跟进了再后续补充,后续也会更新一些多线程的知识以及设计模式等内容,希望大家多多体谅和包涵

最近学校讲到栈和队列了,过段时间应该也会继续更进完善栈和队列相关的博客

Original: https://www.cnblogs.com/melojun/p/15246017.html
Author: Melo~
Title: 想入门数据结构,却总是拜倒在链表的石榴裙下?

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/572234/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 分享自研实现的多数据源(支持同DB不同表、跨DB表、内存数据、外部系统数据等)分页查询工具类实现原理及使用

    思考: 提起分页查询,想必任何一个开发人员(不论是新手还是老手)都能快速编码实现,实现原理再简单不过,无非就是写一条SELECT查询的SQL语句,ORDER BY分页排序的字段, …

    Java 2023年6月9日
    085
  • 220_RabbitMQ-高级-集群

    RabbitMQ 集群 集群搭建 单机多实例搭建 启动第一个节点rabbit-1 启动第二个节点rabbit-2 验证启动 “ps aux|grep rabbitmq&…

    Java 2023年6月7日
    0114
  • synchronized原理剖析

    synchronized原理剖析 并发编程存在什么问题? 1️⃣ 可见性 可见性:是指当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值。 案例演示:一个线程…

    Java 2023年6月15日
    058
  • spring框架 技术方面

    一:依赖注入的两种方式1.set注入2.构造器注入 二:web应用开发中,如何启用spring框架支持,写出关键配置在webApp中获得XMLWebApplicationConte…

    Java 2023年6月5日
    085
  • Docker常用命令简单汇总

    posted @2021-07-30 00:12 萨科拉 阅读(32 ) 评论() 编辑 Original: https://www.cnblogs.com/sakela/p/15…

    Java 2023年6月16日
    071
  • ICMP 介绍

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Java 2023年6月9日
    054
  • list对象中的数据如何去重呢?

    下文笔者讲述list对象的去重方法分享,list的实现类是我们存储数据的容器, 当里面存储的对象存在重复值时,我们该如何对其进行去重操作呢? 下文笔者将一一道来,首先我们需了解对象…

    Java 2023年6月15日
    098
  • Java架构师学习路线思维导图+Java基础+Java常用技术思维导图

    最近浏览保存的一些比较详细的思维导图,大家感兴趣可下载阅读。 Java架构师学习路线思维导图 链接: _ https://www.processon.com/view/link/5…

    Java 2023年5月29日
    0101
  • 自己动手实现AQS(一) AQS互斥模式与ReentrantLock可重入锁原理解析

    MyAQS介绍 在这个系列博客中,我们会参考着jdk的AbstractQueuedLongSynchronizer,从零开始自己动手实现一个AQS(MyAQS)。通过模仿,自己造轮…

    Java 2023年6月8日
    072
  • 获取Spring中@PathVariable注解里带点的完整参数

    背景 原因 解决 参考 背景 spring-boot&#x7684;&#x7248;&#x672C;&#x662F;2.1.4.RELEASE&am…

    Java 2023年6月8日
    098
  • Docker实用篇

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Java 2023年6月13日
    070
  • Mybatis 实体别名支持通配符扫描

    问题 Spring集成Mybatis的项目中,可以为指定包下的实体取别名,这样在Mapper xml文件中可以省略实体类的全路径名称,只写类名称即可;但是在多模块项目中,可能需要将…

    Java 2023年6月5日
    050
  • 【Spring Boot】我的第一个Spring Boot练习

    背景 Spring 在 Java 生态的企业级开发项目中极其常用,通常我们为项目引入一项新技术时,不得不考虑如何将新技术 与 Spring 整合在一起。 我们知道,预研一项新技术,…

    Java 2023年5月29日
    079
  • [Redis] Redis的三大缓存异常原因分析和解决方案

    Redis的三大缓存异常原因分析和解决方案 缓存的三个异常分别是缓存击穿、缓存雪崩、缓存穿透。这三个问题一旦发生,会导致大量的请求积压到数据库层,并发量巨大的情况下很有可能导致数据…

    Java 2023年6月5日
    061
  • RocketMq 完整部署

    RocketMq 部署 环境 物理机部署 自定义日志目录 自定义参数和数据存放位置 服务启动 启动name server 启动broker 关停服务 尝试发送消息 常见报错 部署 …

    Java 2023年6月6日
    0102
  • mongodb在双活(主备)机房的部署方案和切换方案设计

    概述 现在很多高可用系统为了应对极端情况,比如主机宕机、网络故障以及机房宕机等灾难的发生,通常会部署主备架构(双机房),或者双活架构(双机房),甚至多活架构(三个机房或者以上),m…

    Java 2023年6月6日
    081
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球