算法系列--链表问题

一.一些经验总结

  1. 链表天然具有递归性质,单链表可以看做一个单叉树,很多可以应用到二叉树的题目也可以应用到链表的题目之中,下面是一个体现单链表递归性质很好的例子逆序打印链表的值
java 复制代码
private void reversePrint(ListNode head) {
	if(head == null) return;
	reversePrint(head.next);
	System.out.println(head.val)
}

不难发现这种打印方式很像二叉树中的后序遍历

  1. 对于链表的题目,思路往往很容易想到,只是过程可能有点复杂,一定要多画图,一定要舍得用变量

二.例题讲解

01.删除排序链表中重复的元素

链接:https://leetcode.cn/problems/remove-duplicates-from-sorted-list/description/
分析

  • 这是链表去重的经典问题,有多种解法
  • 最容易想到的解法就是使用一个去重的数据结构(Set),遍历整个链表
  • 最优秀的解法是一个指针,一次遍历,注意题目条件,数组是有序的,那么重复元素一定是相邻的,每遍历到一个节点,就判断cur.val == cur.next.val,如果相等,就删除cur.next(完成一次删除操作);如果不等,直接让cur向后走一步即可

代码:

java 复制代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if(head == null) return null;
        // 原地去重
        ListNode cur = head;
        while(cur.next != null) {
            if(cur.val ==cur.next.val) cur.next = cur.next.next;
            else cur = cur.next;// 有可能直接走到null
        }
        return head;
    }
}

说明:

  1. 循环的条件往往是根据下面的判断条件决定的,判断条件是cur.val == cur.next.val,如果cur.next ==null,则会触发空指针异常,所以循环的条件是cur.next != null,对于链表的最后一个元素,要么是重复元素,要么不是重复元素,如果是重复元素,则前一个节点的值和当前节点的值相等,在上一步就会执行删除操作;如果不是重复元素,不用删除
  2. 为什么存在重复元素的情况,删除重复元素之后不让cur走一步?因为有可能删除的是最后一个元素,这样cur.next = null,如果让cur向后走一步,在下一步的循环判断中就会触发空指针异常

删除重复元素的进阶版

链接:https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/description/
分析

  • 本题相较于上一题,需要将所有的重复元素都删除,而上一题是只保留重复元素的一个
  • 同样也可以采用一个指针,一次遍历的操作,不过本题要删除所有的重复元素,就不能让指针走到重复元素的位置,应该走到第一个重复元素的前去节点,即判断cur.next.val == cur.next.next.val,如果相等,则一直循环删除所有等于cur.next.val(设为x)的所有节点,直到值不为x
  • 循环条件和判断条件的确立同上一题

代码:

java 复制代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        // 个人经验  使用一个指针更容易理解  反而维护多个指针容易把自己绕晕
        if(head == null) return null;
        // 一次遍历的思路
        ListNode phead = new ListNode(0);
        phead.next = head;
        ListNode cur = phead;

        while(cur.next != null && cur.next.next != null) {
            if(cur.next.val == cur.next.next.val) {
                int x = cur.next.val;
                while(cur.next != null && cur.next.val == x)// 一直走到值不等于x的节点
                    cur.next = cur.next.next;
            }else {
                cur = cur.next;
            }
        }
        return phead.next;

    }
}

02.反转链表

链接:https://leetcode.cn/problems/reverse-linked-list/
分析

1.方法一:使用两个指针迭代完成局部的链表的反转

java 复制代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        if(head == null) return null;
        ListNode pre = null, cur = head;
        while(cur != null) {
            ListNode tmp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;// pre此时是原链表的最后一个节点  反转链表的头结点
    }
}

2.方法2:递归写法

  • 你给我反转当前节点(head)后面的所有节点,并且把反转后的头节点返回
  • 反转当前节点和后面的节点,将后面的节点当做一个节点即可
java 复制代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        if(head == null || head.next == null) return head;

        ListNode newHead = reverseList(head.next);

        // 完成一次局部的反转
        head.next.next = head;
        head.next = null;
        return newHead;
    }
}
  • 为什么返回newHead而不是head?因为newHead是完成反转之后的新的头结点

03.回文链表

链接:https://leetcode.cn/problems/palindrome-linked-list/description/
分析

方法1:栈

遇到对称有关的问题应该先考虑能否使用stack这种数据结构解决,对称问题最大的特点就是从前往后遍历的结果和从后往前遍历的结果相同,从前往后遍历容易,关键在于对于某些问题从后往前遍历很困难(比如单链表),这是就可以使用栈这种数据结构,充分利用栈后进先出的结构特点完成从后往前遍历

java 复制代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public boolean isPalindrome(ListNode head) {
        // 借助栈
        Stack<Integer> st = new Stack<>();
        ListNode i1 = head;

        // 1.将所有元素入栈
        while(i1 != null) {
            st.push(i1.val);
            i1 = i1.next;
        }

        // 2.依次出栈进行比较
        ListNode i2 = head;
        while(i2 != null) {
            if(st.peek() != i2.val) return false;
            else {
                i2 = i2.next;
                st.pop();
            }
        }
        return true;
    }
}

方法2:反转后半部分链表,依次进行比较

  1. 利用快慢指针找到中间节点
  2. 根据fast是否为null判断节点的个数是奇数还是偶数,如果是奇数,向前走一步:如果是偶数,无需移动
  3. 反转后面的所有节点,依次向后比较
java 复制代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 * int val;
 * ListNode next;
 * ListNode() {}
 * ListNode(int val) { this.val = val; }
 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public boolean isPalindrome(ListNode head) {
        ListNode fast = head, slow = head;
        // 通过快慢指针找到中点
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        // 如果fast不为空,说明链表的长度是奇数个
        if (fast != null) {
            slow = slow.next;
        }
        // 反转后半部分链表
        slow = reverse(slow);

        fast = head;
        while (slow != null) {
            // 然后比较,判断节点值是否相等
            if (fast.val != slow.val)
                return false;
            fast = fast.next;
            slow = slow.next;
        }
        return true;
    }

    // 反转链表
    public ListNode reverse(ListNode head) {
        if(head == null || head.next == null) return head;
        ListNode newHead = reverse(head.next);
        head.next.next = head;
        head.next = null;
        return newHead;
    }

}
相关推荐
寂静山林8 分钟前
UVa 11855 Buzzwords
算法
Curry_Math13 分钟前
LeetCode 热题100之技巧关卡
算法·leetcode
ahadee20 分钟前
蓝桥杯每日真题 - 第10天
c语言·vscode·算法·蓝桥杯
军训猫猫头1 小时前
35.矩阵格式的一到一百数字 C语言
c语言·算法
Mr_Xuhhh2 小时前
递归搜索与回溯算法
c语言·开发语言·c++·算法·github
SoraLuna2 小时前
「Mac玩转仓颉内测版12」PTA刷题篇3 - L1-003 个位数统计
算法·macos·cangjie
爱吃生蚝的于勒4 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
ChoSeitaku9 小时前
链表循环及差集相关算法题|判断循环双链表是否对称|两循环单链表合并成循环链表|使双向循环链表有序|单循环链表改双向循环链表|两链表的差集(C)
c语言·算法·链表
Fuxiao___9 小时前
不使用递归的决策树生成算法
算法
我爱工作&工作love我9 小时前
1435:【例题3】曲线 一本通 代替三分
c++·算法