0
点赞
收藏
分享

微信扫一扫

链表问题不会做?LC狂刷50道链表算法总结出这9道典型题,套路很简单(二)

沪钢木子 2021-10-15 阅读 3
日记本DSA

前言

续上前篇链表典型题。错过的朋友可以关注我,在我的主页查看学习。废话不多说,我们直接上干货。(很干,建议备好白开水)

5、将单向链表按某值划分成左边小,中间相等,右边大的形式md

【题目描述】

给定一个单向链表的头结点head,节点的值类型是整型,再给定一个整数privot。实现一个调整链表的函数,将链表调整为左部分都是值小于privot的节点,中间部分都是值等于privot的节点,右部分都是大于privot的节点。且对某部分内部节点的顺序不做要求

例如:链表9-0-4-5-1,pivot=3。

调整后是1-0-4-9-5,

也可以是0-1-9-5-4

【要求】

如果链表的长度为 N, 时间复杂度达到 O(N)。

【解答】

这道题在思路上还是比较简单的,但是在实现上还是有一些细节需要主要的。

本题对某部分的内部节点不做要求,一种很简单的方法就是用一个数组来存链表的节点,然后像类似于快速排序的分割函数那样,按照某个值把他们进行划分。

不过这样做的话,空间复杂度为 O(N)。我们也可以采取使用3个指针,把原链表依次划分成三个部分的链表,然后再把他们合并起来,这种做法不但空间复杂度为 O(1), 而且内部节点的顺序也是和原链表一样的。虽然思路简单,但在代码实现上也是有很多细节需要注意的,有时间的话希望大家动手打下码。

代码如下:

//用三个指针处理,这道题主要是要注意串联链表时的一些细节处理
    public static Node listPartition(Node head, int pivot) {
        Node sB = null;//小的指针头,即small begin
        Node sE = null;//小的指针尾,即 small end
        Node eB = null;//中的指针头,即 equal begin
        Node eE = null;//中的指针尾,即emall end
        Node bB = null;//大的指针头,即 big begin
        Node bE = null;//大的指针尾,即 big end
        Node next = null;//保存下一个节点
        //进行划分
        while (head != null) {
            next = head.next;
            head.next = null;
            if (head.value < pivot) {
                if (sB == null) {
                    sB = head;
                    sE = head;
                } else {
                    sE.next = head;
                    sE = sE.next;
                }
            } else if (head.value == pivot) {
                if (eB == null) {
                    eB = head;
                    eE = head;
                } else {
                    eE.next = head;
                    eE = eE.next;
                }
            } else {
                if (bB == null) {
                    bB = head;
                    bE = head;
                } else {
                    bE.next = head;
                    bE = bE.next;
                }
            }
            head = next;
        }
        //把三部分串连起来,串联的时候细节还是挺多的,
        //串联的过程下面代码的精简程度是最学习的部分了

        //1.小的与中的串联
        if (sB != null) {
            sE.next = eB;
            eE = eE == null ? sE : eE;
        }
        //2.中的和大的连接
        if (eB != null) {
            eE.next = bB;
        }
        return sB != null ? sB : eB != null ? eB : bB;
    }

6、复制含有随机指针节点的链表

【题目描述】

【要求】

如果链表的长度为 N, 时间复杂度达到 O(N)。

【解答】

方法一:使用额外的存储空间

这道题的难点在于我们需要定位好随机指针,一个比较简单的解法就是把原节点与复制的节点关联起来,可以使用哈希表把他们关联起来。

首先把副节点全部创建出来,然后把原节点与对应的副节点用哈希表关联起来。关联的时候原节点作为key,副节点作为value。例如对于链表 1->2->3->null。创建副节点 1', 2', 3'。然后用哈希表关联起来:

key | value ---|--- 1 | 1' 2 | 2' 3 | 3'

之后在把所有副节点连接成一个链表。在连接的时候,我们 可以通过哈希表很容易这找到对应的随机节点。

代码如下

//方法1:采用哈希表
    public static Node1 copyListWithRand(Node1 head) {
        Map<Node1, Node1> map = new HashMap<>();
        Node1 cur = head;
        while (cur != null) {
            map.put(cur, new Node1(cur.value));
            cur = cur.next;
        }
        //把副节点连接起来
        cur = head;
        while (cur != null) {
            map.get(cur).next = map.get(cur.next);
            map.get(cur).rand = map.get(cur.rand);
            cur = cur.next;
        }
        return map.get(head);
    }

这种方法的时间复杂度为 O(n), 空间复杂度也为 O(n)。

方法2

其实我们也可以不需要哈希表来辅助,也就是说 ,我们是可以做到空间复杂度为 O(1)的,我们可以把复制的副节点插入到原链表中去,这样也能把原节点与副节点进行关联,进而 定位到随机节点。例如,对于链表 1->2->3->null。首先生成副节点 1', 2', 3。然后把副节点插入到原节点的相邻位置,即把原链表变成 1->1'->2->2'->3->3'->null。

这样我们也可以在连接副节点的时候,找到相应的随机节点。例如 1 的随机节点是 3,则 1' 的随机节点是 3'。显然,1节点的随机节点的下一个节点就是 1'的随机节点。具体代码如下:

//方法二
    public static Node1 copyListWithRand2(Node1 head){
        Node1 cur = head;
        Node1 next = null;

        //把复制的节点插进去
        while (cur != null) {
            next = cur.next;
            Node1 temp = new Node1(cur.value);//复制节点
            temp.next = cur.next;
            cur.next = temp;
            cur = next;
        }
        //在一边把复制的节点取出来一边连接。
        cur = head;
        next = null;
        while (cur != null) {
            next = cur.next.next;//保存原链表的下一个节点
            cur.next.next = next != null ? next.next : null;
            cur.next.rand = cur.rand != null ? cur.rand.next : null;
            cur = next;
        }
        return head.next;
    }

采用这种方法的时候,由于随机节点有可能是空指针,随意写代码的时候要注意。

7、将单链表的每K个节点之间逆序

【题目描述】

给定一个单链表的头节点head, 实现一个调整单链表的函数,使得每K个节点之间逆序,如果最后不够K个节点一组,则不调整最后几个节点。

例如:

链表:1->2->3->4->5->6->7->8->null, K = 3。

调整后:3->2->1->6->5->4->7->8->null。其中 7,8不调整,因为不够一组。

【要求】

如果链表的长度为 N, 时间复杂度达到 O(N)。

【解答】

这道题我们可以用递归来实现,假设方法reverseKNode()的功能是将单链表的每K个节点之间逆序。reverse()方法的功能是将一个单链表逆序。

那么对于下面的这个单链表,其中 K = 3。


我们把前K个节点与后面的节点分割出来:

temp指向的剩余的链表,可以说是原问题的一个子问题。我们可以调用reverseKNode()方法将temp指向的链表每K个节点之间进行逆序。再调用reverse()方法把head指向的那3个节点进行逆序,结果如下:

接着,我们只需要把这两部分给连接起来就可以了。最后的结果如下:

如果不大理解,看下代码可能就比较好理解了。

代码如下

//每k个节点为一组的逆转
    public static Node reverseKNodes(Node head, int k) {
        if (head == null || head.next == null) {
            return head;
        }
        Node cur = head;
        for (int i = 1; cur != null && i < k; i++) {
            cur = cur.next;
        }
        //判断是否能组成一组。
        if (cur == null) {
            return head;
        }
        //temp指向剩余的链表
        Node temp = cur.next;
        cur.next = null;
        //把k个节点进行反转
        Node newHead = reverse(head);
        //把之后的部分链表进行每K个节点逆转转
        Node newTemp = reverseKNodes(temp, k);
        //把两部分节点连接起来
        return newHead;
    }

        //单链表逆序
    public static Node reverse(Node head) {
        if (head == null || head.next == null) {
            return head;
        }
        Node newHead = reverse(head.next);
        head.next.next =  head;
        head.next = null;
        return newHead;
    }

当然,这道题一个很简单的做法就是利用栈来辅助,每K个节点入栈就把这K个节点出栈连接成一个链表,之后剩余再在进栈.....

不过这种做法的额外空间复杂度是O(K)。

8、将搜索二叉树转换成双向链表

【题目描述】

对于二叉树的节点来说,有本身的值域,有指向左孩子和右孩子的两个指针;对双向链表的节点来说,有本身的值域,有指向上一个节点和下一个节点的指针。在结构上,两种结构有相似性,现有一棵搜索二叉树,请将其转为成一个有序的双向链表。   节点定义:

class Node2{
    public int value;
    public Node2 left;
    public Node2 right;

    public Node2(int value) {
        this.value = value;
    }
}

例如:



这棵二叉搜索树转换后的双向链表从头到尾依次是 1~9。对于每一个节点来说,原来的 right 指针等价于转换后的 next 指针,原来的 left 指针等价于转换后的 last 指针,最后返回转换后的双向链表的头节点。

【要求】

如果链表的长度为 N, 时间复杂度达到 O(N)

【解答】

方法一:采用队列辅助

如果用一个队列来辅助的话,还是挺容易。采用中序遍历的方法,把二叉树的节点全部放进队列,之后在逐一弹出来连接成双向链表。

代码如下

public static Node2 convert1(Node2 head) {
        Queue<Node2> queue = new LinkedList<>();
        //将节点按中序遍历放进队列里
        inOrderToQueue(head, queue);
        head = queue.poll();
        Node2 pre = head;
        pre.left = null;
        Node2 cur = null;
        while (!queue.isEmpty()) {
            cur = queue.poll();
            pre.right = cur;
            cur.left = pre;
            pre = cur;
        }
        pre.right = null;
        return head;
    }

    private static void inOrderToQueue(Node2 head, Queue<Node2> queue) {
        if (head == null) {
            return;
        }
        inOrderToQueue(head.left, queue);
        queue.offer(head);
        inOrderToQueue(head.right, queue);
    }

这种方法的时间复杂度为 O(n), 空间复杂度也为 O(n)。

方法2:通过递归的方式

在之前打卡的9道题中,几乎超过一般都用到了递归,如果这些题目使用的递归大家都理解了,并且能够自己独立写出代码了,那么我相信大家对递归的思想、使用已经有一定的熟练性。

我们假设函数conver的功能就是把二叉树变成双向链表,例如对于这种一棵二叉树:

经过conver转换后变成这样:

注意,转换之后,把最右边节点的right指针指向了最左边的节点的。

对于下面这样一颗二叉树:

采用conver函数分别对左右子树做处理,结果如下:

之后,再把他们连接起来

了解了基本原理之后,直接看代码吧。


public static Node2 conver(Node2 head) {
        if (head == null) {
            return head;
        }
        Node2 leftE = conver(head.left);
        Node2 rightE = conver(head.right);
        Node2 leftB = leftE != null ? leftE.right : null;
        Node2 rightB = rightE != null ? rightE.right : null;
        if (leftE != null && rightE != null) {
            leftE.right = head;
            head.left = leftE;
            head.right = rightB;
            rightB.left = head;
            rightE.right = leftB;
            return rightE;
        } else if (leftE != null) {
            leftE.right = head;
            head.left = leftE;
            head.right = leftB;
            return head;
        } else if (rightE != null) {
            head.right = rightB;
            rightB.left = head;
            rightE.right = head;
            return rightE;
        } else {
            head.right = head;
            return head;
        }
    }

时间复杂度为O(n),空间复杂度为O(h),其中h是二叉树的高度。

原理虽然不难,但写起代码,还是有挺多细节需要注意的,所以一直强调,有时间的话,一定要自己手打一遍代码,有时你以为自己懂了,可能在写代码的时候,发现自己并没有懂,一写就出现很多bug。

9、删除单链表的第 K个节点

【题目描述】

在单链表中删除倒数第 K 个节点

【要求】

如果链表的长度为 N, 时间复杂度达到 O(N), 额外空间复杂度达到 O(1)

【解答】

删除的时候会出现三种情况:

1、不存在倒数第 K 个节点,此时不用删除

2、倒数第 K 个节点就是第一个节点

3、倒数第 K 个节点在第一个节点之后

所以我们可以用一个变量 sum 记录链表一共有多少个节点。

如果 num < K,则属于第一种情况。

如果 num == K,则属于第二中情况。

如果 num > K, 则属于第三种情况,此时删除倒数第 K 个节点等价于删除第 (num - k + 1) 个节点。
代码如下:

//节点
class Node{
    public int value;
    public Node next;
    public Node(int data) {
        this.value = data;
    }
}

public class 删除倒数第K个节点 {
    public Node removeLastKthNode(Node head, int K) {
        if(head == null || K < 1)
            return head;
        Node temp = head;
        int num = 0;
        while (temp != null) {
            num++;
            temp = temp.next;
        }
        if (num == K) {
            return head.next;
        }
        if (num > K) {
            temp = head;
            //删除第(num-k+1)个节点
            //定位到这个点的前驱
            while (num - K != 0) {
                temp = temp.next;
                num--;
            }
            temp.next = temp.next.next;
        }
        return head;
    }
}

文末

好了,恭喜你完成了链表算法的深入学习,如果对您有用,烦请看客老爷们给我点一波关注、转发、收藏三连一下。



原文:帅地

举报

相关推荐

0 条评论