LeetCode刷题:链表

基础操作:设计链表

引导

题目

public class ListNode {
    
    
  int val;
  ListNode next;
  ListNode(int x) {
    
     val = x; }
}

class MyLinkedList {
    
    
  int size;
  ListNode head;  // sentinel node as pseudo-head
  public MyLinkedList() {
    
    
    size = 0;
    head = new ListNode(0);
  }

  /** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */
  public int get(int index) {
    
    
    // if index is invalid
    if (index < 0 || index >= size) return -1;

    ListNode curr = head;
    // index steps needed 
    // to move from sentinel node to wanted index
    for(int i = 0; i < index + 1; ++i) curr = curr.next;
    return curr.val;
  }

  /** Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. */
  public void addAtHead(int val) {
    
    
    addAtIndex(0, val);
  }

  /** Append a node of value val to the last element of the linked list. */
  public void addAtTail(int val) {
    
    
    addAtIndex(size, val);
  }

  /** Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. */
  public void addAtIndex(int index, int val) {
    
    
    // If index is greater than the length, 
    // the node will not be inserted.
    if (index > size) return;

    // [so weird] If index is negative, 
    // the node will be inserted at the head of the list.
    if (index < 0) index = 0;

    ++size;
    // find predecessor of the node to be added
    ListNode pred = head;
    for(int i = 0; i < index; ++i) pred = pred.next;

    // node to be added
    ListNode toAdd = new ListNode(val);
    // insertion itself
    toAdd.next = pred.next;
    pred.next = toAdd;
  }

  /** Delete the index-th node in the linked list, if the index is valid. */
  public void deleteAtIndex(int index) {
    
    
    // if the index is invalid, do nothing
    if (index < 0 || index >= size) return;

    size--;
    // find predecessor of the node to be deleted
    ListNode pred = head;
    for(int i = 0; i < index; ++i) pred = pred.next;

    // delete pred.next 
    pred.next = pred.next.next;
  }
}

虚拟头结点

引导
链表操作的两种方式:

「直接使用原来的链表来进行删除操作。」
「设置一个虚拟头结点在进行删除操作。」
移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。
所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。

「可以设置一个虚拟头结点」,这样原链表的所有节点就都可以按照统一的方式进行移除了。

例题1

题目链接

/**
 * 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 removeElements(ListNode head, int val) {
    
    
    ListNode sentinel = new ListNode(0);
    sentinel.next = head;

    ListNode prev = sentinel, curr = head; // 只有next,因此需要两个node执行
    while (curr != null) {
    
    
      if (curr.val == val) prev.next = curr.next;
      else prev = curr;
      curr = curr.next;
    }
    return sentinel.next;
  }
}

双指针/节点的操作

与数组双指针比起来,多了链的操作。

例题1:反转链表

LeetCode题目206,最基础的双指针/节点处理,相邻两个节点处理链的拆与合

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

例题2:删除倒数第N个节点

题解链接
在这里插入图片描述

扫描二维码关注公众号,回复: 12909149 查看本文章

例题两数相加:基本的head、tail操作

  1. /10取进位与%10取留下的数字
  2. head与tail,返回head用tail去一直处理
    在这里插入图片描述
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    
    
        ListNode head = null, tail = null;
        int carry = 0;
        // 避免2个while,也让99999 + 999这种情形时最后能够一直向后运算,最后的sum只需要再处理一次即可
        while (l1 != null || l2 != null) {
    
    
	        // null用0补
            int n1 = l1 != null ? l1.val : 0;
            int n2 = l2 != null ? l2.val : 0;
            int sum = n1 + n2 + carry;
            // head是哨兵
            if (head == null) {
    
    
           		// 第一次,可以使用虚拟头结点进行处理
                head = tail = new ListNode(sum % 10);
            } else {
    
    
            	// 非第一次
                tail.next = new ListNode(sum % 10);
                tail = tail.next;
            }
            carry = sum / 10;
            if (l1 != null) {
    
    
                l1 = l1.next;
            }
            if (l2 != null) {
    
    
                l2 = l2.next;
            }
        }
        if (carry > 0) {
    
    
            tail.next = new ListNode(carry);
        }
        return head;
    }

高频:LRU缓存。map存储,链表维护LRU顺序

无赖写法,直接利用LinkedHashMap的能力。
LinkedHashMap是HashMap的子类,但是内部还有一个双向链表维护键值对的顺序,每个键值对既位于哈希表中,也位于双向链表中。LinkedHashMap支持两种顺序插入顺序 、 访问顺序(即遍历map时的顺序)
【插入顺序】:先添加的在前面,后添加的在后面。修改操作不影响顺序
【访问顺序】:所谓访问指的是get/put操作,对一个键执行get/put操作后,其对应的键值对会移动到链表末尾,所以最末尾的是最近访问的,最开始的是最久没有被访问的,这就是访问顺序。(头部结点为最不常访问结点)
LinkedHashMap有5个构造方法,其中4个都是按插入顺序,只有一个是可以指定按访问顺序:(可以用于实现LRU缓存)

public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
// 其中参数accessOrder就是用来指定是否按访问顺序,如果为true,就是访问顺序。
class LRUCache extends LinkedHashMap<Integer, Integer>{
    
    
    private int capacity;
    
    public LRUCache(int capacity) {
    
    
    	// 设置为true即开启LRU缓存
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    public int get(int key) {
    
    
        return super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
    
    
        super.put(key, value);
    }

    @Override
    // 必须实现,否则无法移除
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
    
    
        return size() > capacity; 
    }
}

public class LRUCache {
    
    
    class DLinkedNode {
    
    
    	// 维持内部的LRU顺序,下面的map只是get、put存储的内存结构
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {
    
    }
        public DLinkedNode(int _key, int _value) {
    
    key = _key; value = _value;}
    }

    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    private int size; // 动态容量
    private int capacity; // 固定的容量
    private DLinkedNode head, tail; // head与tail,tail尾缀,head是第一个节点

    public LRUCache(int capacity) {
    
    
        this.size = 0;
        this.capacity = capacity;
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
    
    
        DLinkedNode node = cache.get(key);
        if (node == null) {
    
    
            return -1;
        }
        // 如果 key 存在,先通过哈希表定位,再移到头部
        moveToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
    
    
        DLinkedNode node = cache.get(key);
        if (node == null) {
    
    
            // 如果 key 不存在,创建一个新的节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            // 添加进哈希表
            cache.put(key, newNode);
            // 添加至双向链表的头部
            addToHead(newNode);
            ++size;
            if (size > capacity) {
    
    
                // 如果超出容量,删除双向链表的尾部节点
                DLinkedNode tail = removeTail();
                // 删除哈希表中对应的项
                cache.remove(tail.key);
                --size;
            }
        }
        else {
    
    
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            node.value = value;
            moveToHead(node);
        }
    }

    private void addToHead(DLinkedNode node) {
    
    
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(DLinkedNode node) {
    
    
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(DLinkedNode node) {
    
    
        removeNode(node);
        addToHead(node);
    }

    private DLinkedNode removeTail() {
    
    
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
}

排序链表:归并排序(递归 + 快慢指针选取中间节点 + 合并两个有序链表)

在这里插入图片描述
通过递归实现链表归并排序,有以下两个环节:

分割 cut 环节:
找到当前链表中点,并从中点将链表断开(以便在下次递归 cut 时,链表片段拥有正确边界);
我们使用 fast,slow 快慢双指针法,奇数个节点找到中点,偶数个节点找到中心左边的节点。
找到中点 slow 后,执行 slow.next = None 将链表切断。
递归分割时,输入当前链表左端点 head 和中心节点 slow 的下一个节点 tmp(因为链表是从 slow 切断的)。
cut 递归终止条件: 当head.next == None时,说明只有一个节点了,直接返回此节点。

合并 merge 环节: 将两个排序链表合并,转化为一个排序链表。
双指针法合并,建立辅助ListNode h 作为头部。
设置两指针 left, right 分别指向两链表头部,比较两指针处节点值大小,由小到大加入合并链表头部,指针交替前进,直至添加完两个链表。
返回辅助ListNode h 作为头部的下个节点 h.next。
时间复杂度 O(l + r),l, r 分别代表两个链表长度。

class Solution {
    
    
    public ListNode sortList(ListNode head) {
    
    
        // 1、递归结束条件,拆到一个节点为止
        if (head == null || head.next == null) {
    
    
            return head;
        }

        // 2、找到链表中间节点并断开链表(直接拆分) & 递归下探
        ListNode midNode = middleNode(head);
        ListNode rightHead = midNode.next;
        midNode.next = null;

        ListNode left = sortList(head);
        ListNode right = sortList(rightHead);

        // 3、当前层业务操作(合并2个有序链表:无论是一个节点的还是多个节点出来的)
        return mergeTwoLists(left, right);
    }
    
    //  找到链表中间节点(876. 链表的中间结点),快慢指针找到中间节点
    private ListNode middleNode(ListNode head) {
    
    
        if (head == null || head.next == null) {
    
    
            return head;
        }
        ListNode slow = head;
        ListNode fast = head.next.next;

        while (fast != null && fast.next != null) {
    
    
            slow = slow.next;
            fast = fast.next.next;
        }

        return slow;
    }

    // 合并两个有序链表(21. 合并两个有序链表),这个是最最最基础的
    private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    
    
        ListNode head= new ListNode(-1); // 哨兵,真正的结果是head.next
        ListNode tail = head;

        while(l1 != null && l2 != null) {
    
    
        // 多个链表的三个坐标
            if(l1.val < l2.val) {
    
    
                tail.next = l1;
                l1 = l1.next;
            } else {
    
    
                tail.next = l2;
                l2 = l2.next;
            }

            tail = tail.next;
        }

        tail.next = l1 != null ? l1 : l2;
        return head.next;
}

同类问题:合并K个有序链表
最朴素的方法:用一个变量 ans 来维护以及合并的链表,第 ii 次循环把第 ii 个链表和 ans 合并,答案保存到 ans 中。

class Solution {
    
    
    public ListNode mergeKLists(ListNode[] lists) {
    
    
        ListNode ans = null;
        for (int i = 0; i < lists.length; ++i) {
    
    
            ans = mergeTwoLists(ans, lists[i]);
        }
        return ans;
    }
}

实际上还是归并排序的问题,是其中的一个子规模问题:
即不是深入到单个节点, 而是部分节点已经开始合并为一个list了,接着继续向上排序即可。

class Solution {
    
    
    public ListNode mergeKLists(ListNode[] lists) {
    
    
        return merge(lists, 0, lists.length - 1);
    }

    public ListNode merge(ListNode[] lists, int l, int r) {
    
    
    	// 将每个list视为一个单节点,l、r自然的下标
        if (l == r) {
    
    
            return lists[l];
        }
        if (l > r) {
    
    
            return null;
        }
        int mid = (l + r) >> 1;
        // 合并两个链表
        return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
    }

    public ListNode mergeTwoLists(ListNode a, ListNode b) {
    
    
        if (a == null || b == null) {
    
    
            return a != null ? a : b;
        }
        ListNode head = new ListNode(0);
        ListNode tail = head, aPtr = a, bPtr = b;
        while (aPtr != null && bPtr != null) {
    
    
            if (aPtr.val < bPtr.val) {
    
    
                tail.next = aPtr;
                aPtr = aPtr.next;
            } else {
    
    
                tail.next = bPtr;
                bPtr = bPtr.next;
            }
            tail = tail.next;
        }
        tail.next = (aPtr != null ? aPtr : bPtr);
        return head.next;
    }
}

环形链表1、2

存在环的证明:快慢指针重合即有环;直接到末尾即无环

public class Solution {
    
    
    public boolean hasCycle(ListNode head) {
    
    
        if (head == null || head.next == null) {
    
    
            return false;
        }
        ListNode slow = head;
        ListNode fast = head.next;
        while (slow != fast) {
    
    
            if (fast == null || fast.next == null) {
    
    
                return false;
            }
            slow = slow.next;
            fast = fast.next.next;
        }
        return true;
    }
}

找到进入的节点:head与slow的遇见

// https://leetcode-cn.com/problems/linked-list-cycle-ii/solution/huan-xing-lian-biao-ii-by-leetcode-solution/
public class Solution {
    
    
    public ListNode detectCycle(ListNode head) {
    
    
        if (head == null) {
    
    
            return null;
        }
        ListNode slow = head, fast = head;
        while (fast != null) {
    
    
            slow = slow.next;
            if (fast.next != null) {
    
    
                fast = fast.next.next;
            } else {
    
    
                return null;
            }
            if (fast == slow) {
    
    
                ListNode ptr = head;
                // 寻找进入节点的方法,head + slow遇见
                while (ptr != slow) {
    
    
                    ptr = ptr.next;
                    slow = slow.next;
                }
                return ptr;
            }
        }
        return null;
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_38370441/article/details/115128228