[LeetCode]-链表-1

前言

记录 LeetCode 刷题中遇到的链表相关题目,第一篇

23.合并k个升序链表

基于二路归并,采用分治归并的方法,将链表集合分为两部分,分别将两部分中的链表有序合并,得到两个有序链表,再把这两个有序链表合并

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        return divideAndMerge(lists,0,lists.length - 1);
    }
    public ListNode divideAndMerge(ListNode[] lists,int left,int right){
        if(left > right) return null;
        if(left == right) return lists[left];
        int mid = (left + right) >> 1;
        //把当前链表集合分成两部分,分别合并,各自得到一条有序链表
        ListNode ll = divideAndMerge(lists,left,mid);
        ListNode lr = divideAndMerge(lists,mid + 1,right);
        //再把得到的两条链表合并
        return merge(ll,lr);
    }
    public ListNode merge(ListNode ll,ListNode lr){
        if(ll == null) return lr;
        if(lr == null) return ll;
        ListNode head;
        if(ll.val < lr.val) {
            head = ll;
            ll = ll.next;
        }else{
            head = lr;
            lr = lr.next;
        }
        ListNode tail = head;
        while(ll != null && lr != null){
            if(ll.val < lr.val){
                tail.next = ll;
                tail = tail.next;
                ll = ll.next;
            }else{
                tail.next = lr;
                tail = tail.next;
                lr = lr.next;
            }
        }
        if(ll == null) tail.next = lr;
        else tail.next = ll;
        return head;
    }
}

时间复杂度:每次将集合分为两部分,共能分logk次,每次进行二路归并的复杂度为O(n),则总复杂度为O(nlogk)

剑指 Offer 06. 从尾到头打印链表

注释部分是第一次提交的代码,使用 LinkedList 作为栈,从头到尾遍历链表然后将节点数据入栈,然后再按出栈的顺序放到结果数组中,耗时 1ms。那时候做题都是想着做出来就行,后面开始才 追求最优解

所以二刷的时候就写了另一种做法:直接设置一个足够大的数组,然后按照从头到尾遍历链表的顺序,在数组后面开始往前进行赋值,到最后再对数组有被赋值的部分进行截取复制即可。题目说到链表长度最大 10000.所以数组长度设为10000即可,详见代码

public int[] reversePrint(ListNode head) {
    /*
    LinkedList<Integer> list = new LinkedList<>();
    ListNode p = head;
    while (p != null){
        list.add(p.val);
        p = p.next;
    }
    int[] ans = new int[list.size()];
    int i = list.size() - 1;
    while(i >= 0){
        ans[i--] = list.poll();
    }
    return ans;
    */
    int[] res = new int[10000];
    int index = 9999;
    ListNode p = head;
    while (p != null){
    	//从数组最后面开始赋值
        res[index--] = p.val;
        p = p.next;
    }
    int len = 10000 - index - 1;
    int a[] = new int[len];
    //最后复制有被赋值到的部分即可
    System.arraycopy(res,index + 1,a,0,len);
    return a;
}

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

头节点可能会被修改,而且可能需要多次修改,比如1-1-1-2-…,一开始肯定需要把头节点修改到2的位置,但修改完可能会发现,原链表其实是 1-1-1-2-2-2-3-3-…,所以还要继续判断继续修改,这样处理起来就很麻烦

看了官方题解,其处理方案是 提供一个哑节点 dummy node 作为临时的头节点,这样就不用考虑头节点的问题了,然后借鉴了这个处理方案,写出了下面的0ms耗时代码

public ListNode deleteDuplicates(ListNode head) {
    if(head == null || head.next == null) return head;
    ListNode dummy = new ListNode(-1,head);
    ListNode tmp = dummy;
    ListNode t = null;
    while(tmp.next != null && tmp.next.next != null){
        if(tmp.next.val == tmp.next.next.val){
        	//发现tmp后续的两个节点是相同的,假设值为v,
        	//用t来找到后续第一个值不为v的节点
        	//相当于,tmp后面找到两个重复的,就用t找到后续第一个不重复的
            t = tmp.next.next.next;
            while(t != null && t.val == tmp.next.val){
                t = t.next;
            }
            //tmp的next指向t,表示把tmp跟t中间值为v的所有节点都删掉
            //但不能在这里就后移tmp,因为t的后续节点可能也跟t的值一样
            tmp.next = t;
        }else{ //tmp的后两个节点都不相同时才移动tmp
            tmp = tmp.next;
        }
    }
    return dummy.next;
}

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

public ListNode deleteDuplicates(ListNode head) {
    ListNode dummy = new ListNode(-1,head);
    ListNode tmp = dummy;
    while(tmp.next != null && tmp.next.next != null){
    	//每次判断的是tmp后面两个元素是否重复,如果重复的话删掉其中一个节点
    	//如nummy->1->1->1->2->...,此时tmp在nummy的位置,判断得到tmp后面的两个元素是重复的,所以删掉一个
    	//得到nummy->1->1->2,此时不能直接后移tmp,否则tmp在第一个1的位置,接下来会判断后面的第二个1跟2两个元素是否重复,
    	//那么还有重复的1就被忽略了
        if(tmp.next.val == tmp.next.next.val){
            tmp.next.next = tmp.next.next.next;
        }else{  //所以只有当tmp后面的两个元素不重复时才能后移tmp
            tmp = tmp.next;
        }
    }
    return dummy.next;
}

剑指Offer 18. 删除链表的节点

下面是第一次做的时候提交的代码,对于头节点可能要被删除的情况单独做了分析

public ListNode deleteNode(ListNode head, int val) {
    if(head == null){
        return null;
    }
    //一个结点的特判
    if(head.next == null && head.val != val){
        return head;
    }
    if(head.next == null){
        return null;
    }
    //第一个结点就要被删除的特判
    if(head.val == val){
        return head.next;
    }
    ListNode p = head;
    while (true){
        if(p.next == null){
            return head;
        }
        if(p.next.val == val){
            p.next = p.next.next;
            return head;
        }
        p = p.next;
    }
}

后来做了82题学到了 哑节点 的做法,二刷的时候就用哑节点再做了一遍,代码量明显减少很多,有了哑节点作为临时的头节点,就不用去对原来的头节点需要删除的情况做额外判断,非常方便

public ListNode deleteNode(ListNode head, int val) {
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode tmp = dummy;
    while(tmp != null){
        while(tmp.next != null && tmp.next.val == val){
            tmp.next = tmp.next.next;
        }    
        tmp = tmp.next;
    }
    //最后返回时应记得把哑节点去掉
    return dummy.next;
}

143. 重排链表

以下是 递归 的做法:
对某个链表段调用递归方法,该方法返回该链表段的下一个节点,同时对该链表段进行符合要求的重排。重排的具体操作就是对该链表段中除了首尾节点外的中间的链表段调用递归方法,返回的就是原链表段中的尾结点,然后让头节点指向尾结点,再让尾结点指向中间链表段即可

以下图为例,我们把 segment 看成一个部分,对其调用递归方法,那么会返回 segment 段的下一个节点,即 tail,同时在调用的过程中segment中也已经按照题目要求重排完毕,然后让head指向tail,tail再指向segment即可
链表示意图
用一个更具体的例子:
在这里插入图片描述
为了使这段链表段重排,我们需要对[2,4]调用递归方法。在[2,4]中,继续对[3]调用递归方法,由于只剩一个节点,直接返回其下一个节点,即4,回到[2,4]的状态,此时我们已经通过对[3]调用递归方法得到[2,4]的尾结点为4,那么就可以调整顺序:令2指向4,4指向2后面的3,得到[2,4,3],那么这一段链表重排完毕。不过该方法需要返回这段链表的下一个节点,即5,所以在调整顺序之前我们需要先保存4.next的值,即节点5,然后调整完顺序之后返回5。回到[1,5]的状态,此时已经通过对[2,4]调用递归方法得到了[1,5]的尾结点5,那么就调整顺序:1指向5,5指向1后面的2,4,3,最后得到[1,5,2,4,3],即正确答案

class Solution {
    public void reorderList(ListNode head) {
    	//空链表或单节点或双节点重排后还是本身
        if(head == null || head.next == null || head.next.next == null) return;
        //对链表段的遍历需要知道链表段长度,即遍历多长,因为不需要一直遍历到null。所以要先计算链表长度
        int len = 0;
        ListNode tmp = head;
        while(tmp != null){
            len++;
            tmp = tmp.next;
        }
        rec(head,len);
    }
    //对于t开始(包括t)长度为len的链表段,返回该链表段的下一个节点,同时对链表段本身进行重排
    public ListNode rec(ListNode t,int len){
    	//如果链表段长为1,即原来的链表中最中间的一个节点(原链表长度为奇数),它最后应是重排后的最后一个节点,所以next = null
        if(len == 1){
            ListNode res = t.next;
            t.next = null;
            return res;
        }
        //如果链表段长为2,那么原链表长度为偶数,这两个最中间的节点也不用交换,直接让后面指向null即可。下一个节点就是t.next.next
        else if(len == 2){
            ListNode res = t.next.next;
            t.next.next = null;
            return res;
        }
        //对除首尾节点之外的中间的链表段进行递归,得到t.nedxt开始长度为len-2的链表段的下一个节点,也即当前链表段的最后一个节点tail
        ListNode tail =  rec(t.next,len - 2);
        //调整顺序之前记录下当前链表段的下一个节点作为返回值
        ListNode res = tail.next;
        //调整顺序
        ListNode tmp = t.next;
        t.next = tail;
        tail.next = tmp;
        //最后返回
        return res;
    }
}

以上代码已经算最优解之一了。除此之外还有其它方法,如遍历链表将每个节点存到一个顺序表中,然后对顺序表用头尾双指针遍历,重新连接每一个节点即可;或者,可以找到原链表的中间节点,然后将原链表分为两部分,对后半部分进行反转,然后对前半部分以及反转后的右半部分进行归并也可以

面试题 02.05. 链表求和

两个加数跟答案都是从左到右,从低位到高位。按照最基础的加法运算,从低位往高位相加,即从头到尾遍历两个加数,每遍历一位,进行相加,然后求出进位(如果有的话),到下一位时,除了需要两个节点相加,还要加上上一位的得到的进位

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    ListNode p1 = l1,p2 = l2;
    //设置一个哑节点方便一开始时new出下一个节点
    ListNode dummy = new ListNode(-1);
    ListNode tmp = dummy;
    //记录每一位相加时得到的进位
    int rest = 0;
    while(p1 != null && p2 != null){
    	//计算这一位相加得到的结果
        int v = p1.val + p2.val + rest;
        //计算这一位相加得到的进位
        rest = v / 10;
        //不用进位的部分,即相加得到的结果的个位部分
        v = v % 10;
        tmp.next = new ListNode(v);
        tmp = tmp.next;
        p1 = p1.next;
        p2 = p2.next;
    }
    //考虑到两个链表不等长
    while(p1 != null){
        int v = p1.val + rest;
        rest = v / 10;
        v = v % 10;
        tmp.next = new ListNode(v);
        tmp = tmp.next;
        p1 = p1.next;
    }
    while(p2 != null){
        int v = p2.val + rest;
        rest = v / 10;
        v = v % 10;
        tmp.next = new ListNode(v);
        tmp = tmp.next;
        p2 = p2.next;
    }
    //计算最高位的时候可能还会有进位,有的话要多延伸一位
    if(rest > 0){
        tmp.next = new ListNode(rest);
        tmp = tmp.next;
    }
    tmp.next = null;
    return dummy.next;
}

双向链表

我们常常会把数组和链表作为比较,因为数组的占用空间是连续,且固定长度的,而链表的占用空间是不连续的,而且长度可以随时变化不用考虑空间大小的影响;
数组借助于下标索可以快速定位元素以 O(1) 的时间完成查找操作,而链表,通常说的是单向链表,我们只能通过头节点向后查找,需要 O(n) 的时间完成查找;
数组中删除元素由于长度发生了变化,所以通常需要额外 O(n) 的时间完成原数组没有被删除的内容的复制,而链表只需让被删节点的前后驱节点直接连接即可。

这也是我们通常说的:基于数组的结构便于查找,而基于链表的结构便于增删。而数组借助于下标索引方便查找的操作其实跟哈希表是一个道理的,只不过数组中的键即哈希值是下标,元素值是对应的数组元素,而且一个哈希值只能对应一个元素值。所以归根结底,应该说哈希便于查找

既然如此,当我们需要一种既方便查找,又方便增删的结构时,是不是可以结合这两种存储结构,让数据存储在链表中,对应每一个节点,同时哈希表存储的元素是链表节点,借助哈希表完成查询,当需要删除节点时,也可以迅速找到要删的目标节点然后在链表中删除,实现查找跟删除都是O(1)的时间复杂度。

不过还有一个点,单向链表一个节点只保存了它的后继节点,要删除的话还需要它的前驱节点,所以这里就要把单向链表升级为双向链表,同时,鉴于一般的增加节点即插入节点的操作都是将节点插入到链表末端,所以对于双向链表我们还可以考虑不仅保存它的头节点还保存它的尾结点

总结一下就是,当我们需要实现一种既能快速查询又能快速删除的结构,可以考虑 哈希+双向链表,具体应用就是下面这一道题

146.LRU缓存机制

用哈希加双向链表完成这道题。首先要知道在LRU中,获取内容和新增内容时如何操作。因为一方面内容存储的容量是有限的,一方面存储的内容还要考虑优先级。

在获取内容的操作,即get()方法中,如果想查找的键存在,就返回它对应的值,否则返回-1。而且被查找的内容存在的话还要调整它的优先级至最高,即调整到链表头的位置

插入新内容的操作:第一,对于链表,越靠近链表头的节点优先级越高,反之越靠近链表尾的节点优先级越低,所以在插入节点时应该采用头插入的方法,即后来的内容优先级最高;第二,如果当前想插入的键值对的键已经存在,那么除了要更新这个键的值为最新的值外,还应该把这个键值对,也就是这个键所在的结点调至链表头,使其优先级最高,相当于把键对应的旧的节点删除再加入新的节点。也就是,被更新的内容优先级提至最高

知道 LRU 机制后,接下来就是代码的实现了

class LRUCache {
    class Node{
        int key;
        int val;
        Node front;
        Node next;
        public Node(int key,int val){
            this.key = key;
            this.val = val;
            this.front = null;
            this.next = null;
        }
    }
    class DoubleList{
        Node head;
        Node tail;
        int size;
        int maxSize;
    }
    private DoubleList list;
    private Map<Integer,Node> map;
    public LRUCache(int capacity) {
        list = new DoubleList();
        list.head = list.tail = null;
        list.maxSize = capacity;
        list.size = 0;
        map = new HashMap<>();
    }
    public void deleteNode(Node n){
        map.remove(n.key);
        if(list.size == 1){
            list.head = list.tail = null;
        }else if(list.head == n){
            list.head = n.next;
            list.head.front = null;
        }else{
            if(list.tail == n){
                list.tail = n.front;
                list.tail.next = null;
            }else{
                n.front.next = n.next;
                n.next.front = n.front;
            }
        }
        list.size--;
    }
    public void addHead(Node n){
        if(list.size == list.maxSize){
            deleteNode(list.tail);
        }
        map.put(n.key,n);
        if(list.size == 0){
            list.head = list.tail = n;
        }else{
            n.front = null;
            n.next = list.head;
            list.head.front = n;
            list.head = n;
        }
        list.size++;
    }
    public int get(int key) {
        Node n = map.getOrDefault(key,null);
        if(n == null) return -1;
        if(list.head != n){
            deleteNode(n);
            addHead(n);
        }
        return n.val;
    }
    public void put(int key, int value) {
        Node newNode;
        if((newNode = map.getOrDefault(key,null)) != null){
            deleteNode(newNode);
        }
        newNode = new Node(key,value);
        addHead(newNode);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值