• 【Java 数据结构】单链表与OJ题


    篮球哥温馨提示:编程的同时不要忘记锻炼哦!

    暮色降临🌃,冲一杯咖啡!


    目录

    1、什么是链表?

    2、实现一个单向非循环链表

    2.1 实现前的约定

    2.2 addFirst 方法

    2.3 addList 方法

    2.4 addIndex 方法

    2.5 contains 方法

    2.6 remove 方法

    2.7 removeAllKey 方法

    2.8 size 和 clear 方法

    3、单链表OJ题深度解剖

    3.1 移除链表元素(来源:LeetCode 难度:简单)

    3.2 反转链表(来源:LeetCode 难度:简单)

    3.4 链表中倒数第k个节点(来源:牛客网 难度:简单) 

    3.6 链表分割(来源:牛客网 难度:较难) 

    3.7 链表的回文结构(来源:LeetCode 难度:较难) 

    3.8 相交链表(来源:LeetCode 难度:简单) 


    1、什么是链表?

    链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。

    通俗点,就是每个元素是一个节点,然后用一个指针域给后面的节点连起来,第一个节点没有前驱,最后一个节点没有后继。

    实际中要实现的链表的结构非常多样,以下情况组合起来就有8种链表结构:

    1. 单向、双向         2. 带头、不带头         3. 循环、非循环

    我们重点讲解单向非循环链表和双向非循环链表,同时这两个也是笔试中考的比较多的。

    2、实现一个单向非循环链表

    2.1 实现前的约定

    因为链表的每个元素是一个节点,所以我们采取内部类的方式,而我们还需要定义一个头节点的引用,来始终指向头节点。

    1. public class MySingleList {
    2. private ListNode head; //引用头节点
    3. // 链表每个元素是一个节点
    4. private class ListNode {
    5. private int val; //存放数据元素
    6. private ListNode next; //存放下一个节点地址
    7. //构造方法
    8. public ListNode(int val) {
    9. this.val = val;
    10. }
    11. }
    12. }

     注意:链表最少有两个域,分别是数据域和指针域,当然你也可以有多个数据域和指针域。

    我们还需要实现以下几个常用的方法:

    1. public void addFirst(int data); //头插法
    2. public void addLast(int data); //尾插法
    3. public boolean addIndex(int index,int data); //任意位置插入,第一个数据节点为0号下标
    4. public boolean contains(int key); //查找关键字key是否在单链表当中
    5. public void remove(int key); //删除第一次出现关键字为key的节点
    6. public void removeAllKey(int key); //删除所有值为key的节点
    7. public int size(); //得到单链表的长度
    8. public void clear(); //清空链表

    2.2 addFirst 方法

    1. public void addFirst(int data) {
    2. ListNode newNode = new ListNode(data); //把传过来的值放到新的节点中
    3. newNode.next = this.head; //新节点的next指向头节点
    4. this.head = newNode; //使新节点成为头节点
    5. }

    因为head默认是指向空的,当链表为null,也不影响这个代码的执行,不信你下来画画图咯。

    2.3 addList 方法

    1. public void addLast(int data) {
    2. ListNode newNode = new ListNode(data);
    3. // 如果链表为空的情况
    4. if (this.head == null) {
    5. this.head = newNode;
    6. return;
    7. }
    8. // 先找到最后一个节点
    9. ListNode cur = this.head;
    10. while (cur.next != null) {
    11. cur = cur.next;
    12. }
    13. cur.next = newNode;
    14. }

    2.4 addIndex 方法

    1. //任意位置插入,第一个数据节点为0号下标
    2. private ListNode findIndexPrevNode(int index) {
    3. ListNode cur = this.head;
    4. while (index - 1 != 0) {
    5. cur = cur.next;
    6. index--;
    7. }
    8. return cur;
    9. }
    10. public boolean addIndex(int index,int data) {
    11. // 判断index下标的有效性
    12. if (index < 0 || index > size()) {
    13. return false;
    14. }
    15. // 如果在0下标插入则是头插
    16. if (index == 0) {
    17. addFirst(data); //头插
    18. return true;
    19. }
    20. // 如果在末尾插入则是尾插
    21. if (index == size()) {
    22. addLast(data); //尾插
    23. return true;
    24. }
    25. ListNode newNode = new ListNode(data); //新节点
    26. // 在中间插入
    27. ListNode prevNode = findIndexPrevNode(index); //找到index下标的前一个节点
    28. newNode.next = prevNode.next; //新节点的next被改为index的位置的节点
    29. prevNode.next = newNode; //index位置前一个节点next被改成新节点
    30. return true;
    31. }

    这个代码我们首先需要找到index下标的前一个节点,先让新节点跟index位置的节点绑定起来,在把index的前一个节点与新节点连起来,这个地方说文字是不清楚的,小伙伴们可以下来按照我这个代码画图就能理解了,也可也直接看博主之前的C语言实现数据结构专栏,那里面有图解哈。

    2.5 contains 方法

    1. //查找关键字key是否在单链表当中
    2. public boolean contains(int key) {
    3. // 当链表为空的情况
    4. if (this.head == null) {
    5. return false;
    6. }
    7. ListNode cur = this.head;
    8. // 遍历列表
    9. while (cur != null) {
    10. if (cur.val == key) {
    11. return true; //找到了返回true
    12. }
    13. cur = cur.next;
    14. }
    15. return false; //找不到返回false
    16. }

    思路很简单,遍历一遍链表,找到 cur 为空位置,如果有返回true,没有返回false,交给小伙伴自己下来画图咯。

    2.6 remove 方法

    1. //删除第一次出现关键字为key的节点
    2. public void remove(int key) {
    3. if (this.head == null) {
    4. return;
    5. }
    6. ListNode cur = this.head;
    7. // 如果删除的是key为头节点
    8. if (this.head.val == key) {
    9. this.head = head.next;
    10. return;
    11. }
    12. // 这里不能是cur!=null, 不然会越界!!!
    13. while (cur.next != null) {
    14. // 找到 key 的前一个节点
    15. if (cur.next.val == key) {
    16. //当前的cur为key的前一个节点
    17. cur.next = cur.next.next; //cur链接到key的后一个节点
    18. return;
    19. }
    20. cur = cur.next;
    21. }
    22. }

    这里我们需要找到key的前一个节点,然后进行跟key后面的节点绑定即可,当key对应节点没人引用了,则会被自动回收掉。

    2.7 removeAllKey 方法

    1. //删除所有值为key的节点
    2. public void removeAllKey(int key) {
    3. if (this.head == null) {
    4. return;
    5. }
    6. // 采用前后指针的方法
    7. ListNode cur = this.head;
    8. ListNode prev = this.head;
    9. while (cur != null) {
    10. if (cur.val == key) {
    11. prev.next = cur.next; //跳过key节点指向下一个节点
    12. } else {
    13. prev = cur;
    14. }
    15. cur = cur.next;
    16. }
    17. // 判断第一个节点是不是key
    18. if (this.head.val == key) {
    19. this.head = this.head.next; //head指向下一个节点
    20. }
    21. }

    这里大家伙先自己看看,后面讲解OJ题会有这道题详解的。

    2.8 size 和 clear 方法

    我相信这两个方法就不需要多说了吧?遍历?直接头指针置null?这不就简单了吗,这里就不写了哈,交给各位了!


    3、单链表OJ题深度解剖

    这个才是今天的重头戏,不是篮球哥不画图,是因为前面的图太简单了,小伙伴们结合着代码也能自己画出来,但是,对于OJ题,大家伙下去还是得画图的,相信看完这几道题,你会爱上数据结构的。

    3.1 移除链表元素(来源:LeetCode 难度:简单)

    题目:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

    这个题我们可以用前后指针的思路来做,这样也比较通俗易懂,更适合初学者,大概的思路是这样的:我们可以定义一个cur和first的引用,如果碰到相等,也就是first.val == val,我们则让cur的next指向first的下一个节点,如果不相等,则让cur走到first的位置,最后first往后走一步,图解:

    这里还没有完,如果第一个节点的值也是val呢?所以最后我们别忘了进行一个判断,那么最终的代码是这样的:

    1. public ListNode removeElements(ListNode head, int val) {
    2. if (head == null) {
    3. return null;
    4. }
    5. ListNode cur = head;
    6. ListNode first = head;
    7. while (first != null) {
    8. if (first.val == val) {
    9. cur.next = first.next;
    10. } else {
    11. cur = first;
    12. }
    13. first = first.next;
    14. }
    15. // 判断头节点的值是否也是val
    16. if (head.val == val) {
    17. head = head.next;
    18. }
    19. return head;
    20. }

    3.2 反转链表(来源:LeetCode 难度:简单)

    题目:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 

    这个题我们可以先取到头节点,后续的节点都进行头插法即可?我们取到头节点,并且先将头节点的next置空,但是这样一来,就找不到后面的节点了,所以我们还需要有一个curNext引用来记录要反转节点的下一个节点:

    我们的思路是这样的:首先找到头节点的next置空,cur走到curNext位置,curNext往前走,使得cur位置的next指向头节点,头节点cur再次成为新的头节点,当curNext走到null的时候循环结束。

    1. public ListNode reverseList(ListNode head) {
    2. // 空链表的情况
    3. if (head == null) {
    4. return null;
    5. }
    6. ListNode cur = head;
    7. ListNode curNext = cur.next;
    8. head.next = null;
    9. while (curNext != null) {
    10. cur = curNext;
    11. curNext = curNext.next;
    12. cur.next = head;
    13. head = cur;
    14. }
    15. return head;
    16. }

    3.4 链表中倒数第k个节点(来源:牛客网 难度:简单) 

    题目:输入一个链表,输出该链表中倒数第k个结点。 

    这个题也是很简单的一道题,可以采用前后指针法,先让first指针走k步,走完之后slow和first一起走,这样slow和first之间就相差了k步,当first为null时,slow就是倒数第k个节点,在这个过程中,我们还要判断k的合法性,如果k小于等于0?或者k大于链表的长度呢?于是我们就能写出如下的代码:

    1. public ListNode FindKthToTail(ListNode head,int k) {
    2. // 判断k的合法性
    3. if (k <= 0 || head == null) {
    4. return null;
    5. }
    6. ListNode first = head;
    7. ListNode slow = head;
    8. // 先让first走k步
    9. while (k != 0) {
    10. // k的长度大于链表的长度
    11. if (first == null) {
    12. return null;
    13. }
    14. first = first.next;
    15. k--;
    16. }
    17. // 一起走,当first为null,slow就是倒数第k个节点
    18. while (first != null) {
    19. first = first.next;
    20. slow = slow.next;
    21. }
    22. return slow;
    23. }

    3.6 链表分割(来源:牛客网 难度:较难) 

    题目:现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。 

    这个题的思路我们可以这样做,既然是按照给定的值x进行分割,那么我们设定两个盘子,盘子A放小于x的节点,盘子B放大于x的节点,最后把这两个盘子的节点连起来,放回盘子A的头节点即可!

    1. public ListNode partition(ListNode pHead, int x) {
    2. if (pHead == null) {
    3. return null;
    4. }
    5. ListNode headA = null;
    6. ListNode headB = null;
    7. ListNode curA = null;
    8. ListNode curB = null;
    9. ListNode cur = pHead;
    10. while (cur != null) {
    11. if (cur.val < x) {
    12. // 第一次放入A盘子
    13. if (headA == null) {
    14. headA = cur;
    15. curA = cur;
    16. } else {
    17. curA.next = cur;
    18. curA = cur;
    19. }
    20. } else {
    21. // 第一次放入B盘子
    22. if (headB == null) {
    23. headB = cur;
    24. curB = cur;
    25. } else {
    26. curB.next = cur;
    27. curB = cur;
    28. }
    29. }
    30. cur = cur.next;
    31. }
    32. // 如果A盘子为空
    33. if (headA == null) {
    34. return headB;
    35. }
    36. curA.next = headB;
    37. // 避免B盘子尾节点形成环
    38. if (headB != null) {
    39. curB.next = null;
    40. }
    41. return headA;
    42. }

    3.7 链表的回文结构(来源:LeetCode 难度:较难) 

    题目:对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。

    给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

    这个题有要求的,要求空间复杂度为O(1),并且还得在O(n)的时间复杂度下,那我们就原地解决这个题,我们可以分为三个步骤,首先找到中间节点,然后把中间节点往后的节点进行反转,最后左右两个指针同时往中间走。如果光看下面代码看不懂,可以结合着代码画图才能理解更透彻哦!

    1. public boolean chkPalindrome(ListNode A) {
    2. if (A == null) {
    3. return false;
    4. }
    5. // 只有一个节点的情况
    6. if (A.next == null) {
    7. return true;
    8. }
    9. // 首先找到中间节点
    10. ListNode first = A;
    11. ListNode slow = A;
    12. while (first != null && first.next != null) {
    13. first = first.next.next;
    14. slow = slow.next;
    15. }
    16. // 走到这,slow是链表的中间节点,采用头插法反转slow后续的节点
    17. first = slow.next;
    18. ListNode cur = slow;
    19. while (first != null) {
    20. cur = first;
    21. first = first.next;
    22. cur.next = slow; //链接前一个节点
    23. slow = cur; //更新头节点的位置
    24. }
    25. // 走到这,反转完毕,cur指向最后一个节点,让slow等于A,往中间找
    26. slow = A;
    27. while (slow != cur) {
    28. if (slow.val != cur.val) {
    29. return false;
    30. }
    31. // 偶数的情况下需要特殊判断
    32. if (slow.next == cur) {
    33. return true;
    34. }
    35. slow = slow.next;
    36. cur = cur.next;
    37. }
    38. return true;
    39. }

    3.8 相交链表(来源:LeetCode 难度:简单) 

    题目:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

    题目数据 保证 整个链式结构中不存在环。

    注意,函数返回结果后,链表必须 保持其原始结构 。

    这个题我们可以这样做,长链表先走两个链表的长度差的步数,因为相交之后的节点都是一样的个数,所以走了差值后,就两个链表一起往后走,相等了则就是相交节点。

    1. public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    2. if (headA == null || headB == null) {
    3. return null;
    4. }
    5. ListNode longList = headA; //longList始终记录长的链表
    6. ListNode shortList = headB;
    7. // 分别求出两个链表的长度
    8. int lenA = 0;
    9. int lenB = 0;
    10. ListNode cur = headA;
    11. while (cur != null) {
    12. lenA++;
    13. cur = cur.next;
    14. }
    15. cur = headB;
    16. while (cur != null) {
    17. lenB++;
    18. cur = cur.next;
    19. }
    20. int len = lenA - lenB;
    21. // 如果B链表长于A链表
    22. if (len < 0) {
    23. // 修正相差长度
    24. len = lenB - lenA;
    25. longList = headB; //longList始终记录长的链表
    26. shortList = headA;
    27. }
    28. // 让长链表先走差值len步,然后同步走,相等了即为相交节点
    29. while (len != 0) {
    30. longList = longList.next;
    31. len--;
    32. }
    33. while (longList != shortList) {
    34. longList = longList.next;
    35. shortList = shortList.next;
    36. }
    37. // 如果两个链表走到了null,则没有中间节点返回null,如果有,返回任意一个即可
    38. return longList;
    39. }

    还有其他的链表题在博主的C语言数据结构专栏已经讲解过了哦,感兴趣的可以去看一下:
    五大经典链表OJ题https://blog.csdn.net/m0_61784621/article/details/123448745


    下期预告:【Java 数据结构】双向链表 

  • 相关阅读:
    Kotlin 开发Android app(十一):Android控件RecyclerView
    c++编译的四个阶段
    Qt:线程
    IOS手机耗电量测试
    【C++ 学习】指针与函数与多维数组
    Spring Cloud Gateway学习
    前端基础入门之JS对象 原型 prototype
    软件包管理—源码包管理—源码包与RPM包区别
    BSA-maltose 牛血清白蛋白修饰麦芽糖 BSA-麦芽糖
    【数据结构】单值二叉树 & 相同的树 & 翻转二叉树(五)
  • 原文地址:https://blog.csdn.net/m0_61784621/article/details/127313132