数据结构与算法——LeetCode刷题(第一个月)

线性表基础

链表及经典问题

课堂笔记

链表的基础知识
链表的结构
  • 节点
    • 数据域
    • 指针域
      • 地址
      • 下标(相对地址)
      • 引用
  • 链状结构
    • 通过指针域的值形成一个线性结构
访问链表的时间复杂度
  • 链表不适合快速定位数据,适合动态的插入和删除的场景
  • 查找节点O(n)
  • 插入和删除时O(1)
几种经典的链表实现方式
  • 传统方式:节点+指针
  • 使用数组模拟
    • 指针域与数据域相分离
    • 利用数组存放下标进行索引
链表的典型应用场景
  • 操作系统内的动态内存分配
  • LRU缓存淘汰算法
  • 缓存
    • 缓存是一种高速的数据结构
    • 设备间存在速度差异,可以通过将使用较多的数据存放在高速区域,而将使用较少的内存存放在相对较低速的区域,来对系统进行优化

LeetCode算法题

141. 环形链表
  • 哈希表法:
public boolean hasCycle(ListNode head) {
    Set<ListNode> set = new HashSet<>();
    while (head != null) {
        if (!set.add(head)) return true;
        head = head.next;
    }
    return false;
}
  • 快慢指针法:
public boolean hasCycle(ListNode head) {
    if (head == null) return false;
    ListNode slow = head, fast = head;
    do {
        if (fast == null || fast.next == null) return false;
        slow = slow.next;
        fast = fast.next.next;
    } while (slow != fast);
    return true;
}
142. 环形链表 II
  • 哈希表法:
public ListNode detectCycle(ListNode head) {
    Set<ListNode> set = new HashSet<>();
    while (head != null) {
        if (!set.add(head)) return head;
        head = head.next;
    }
    return null;
}
  • 快慢指针法:
    在这里插入图片描述
public ListNode detectCycle(ListNode head) {
    if (head == null) return null;
    ListNode slow = head, fast = head;
    do {
        if (fast == null || fast.next == null) return null;
        slow = slow.next;
        fast = fast.next.next;
    } while (slow != fast);
    slow = head;
    while (slow != fast) {
        slow = slow.next;
        fast = fast.next;
    }
    return slow;
}
202. 快乐数

【思路】如果能变换到 1 则不能成环,即是快乐数;反之成环,则不是快乐数。

public boolean isHappy(int n) {
    int slow = n, fast = n;
    do {
        slow = getNext(slow);
        fast = getNext(getNext(fast));
    } while (slow != fast && fast != 1);
    return fast == 1;
}
private int getNext(int x) {
    int sum = 0;
    while (x > 0) {
        sum += (x % 10) * (x % 10);
        x /= 10;
    }
    return sum;
}
206. 反转链表

在这里插入图片描述

public ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode hair = null, cur = head, next = null;
    while (cur != null) {
        next = cur.next;
        cur.next = hair;
        hair = cur;
        cur = next;
    }
    return hair;
}
92. 反转链表 II

【思路】核心方法还是链表的反转,并由此封装一个反转链表前 N 个节点的方法。借助虚拟头节点处理边界情况,找到要反转区域的起始位置。

public ListNode reverseBetween(ListNode head, int left, int right) {
    ListNode hair = new ListNode(0, head), cur = hair;
    int n = right - left + 1;
    while (left-- > 1) cur = cur.next;
    cur.next = reverseN(cur.next, n);
    return hair.next;
}
private ListNode reverseN(ListNode head, int n) {
    ListNode pre = null, cur = head, next; 
    while (n-- > 0) {
        next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    head.next = cur;
    return pre;
}
25. K 个一组翻转链表

【思路】虚拟头节点、反转链表前N项

public ListNode reverseKGroup(ListNode head, int k) {
    ListNode hair = new ListNode(0, head), cur = hair, next = null, p = head;
    while (true) {
        for (int i = 0; i < k; i++) {
            if (p == null) return hair.next;
            p = p.next;
        }
        next = cur.next;
        if ((cur.next = reverseN(cur.next, k)) == next) break;
        cur = next;
    }
    return hair.next;
}
private ListNode reverseN(ListNode head, int n) {
    ListNode pre = null, cur = head, next;
    while (n-- > 0) {
        next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    head.next = cur;
    return pre;
}
61. 旋转链表

【思路】1、统计节点个数(cnt),并特殊处理 k 值;2、单向链表首尾连接成环,然后当前指针(原单链表尾节点)再向后走(cnt - k % cnt)位;3、修正头节点位置,并断开环链。

public ListNode rotateRight(ListNode head, int k) {
    if (head == null) return null;
    int cnt = 1;
    ListNode cur = head;
    while (cur.next != null) {
        cur = cur.next;
        cnt++;
    }
    k = cnt - k % cnt;
    cur.next = head;
    while (k-- > 0) {
        cur = cur.next;
    }
    head = cur.next;
    cur.next = null;
    return head;
}
24. 两两交换链表中的节点

【思路】链表节点交换,头节点可能会变,所以需要用到虚拟头节点的技巧
在这里插入图片描述

public ListNode swapPairs(ListNode head) {
    ListNode hair = new ListNode(0, head), cur = hair, one = null, two = null;
    while(cur.next != null && cur.next.next != null) {
        one = cur.next;
        two = one.next;
        one.next = two.next;
        two.next = one;
        cur.next = two;
        cur = one;
    }
    return hair.next;
}
19. 删除链表的倒数第 N 个结点

【思路】1、链表节点的删除,可能会把头节点给删掉,所以需要使用到虚拟头节点的技巧;2、先定义一个指针 p 向后走 n 步,定义指针 cur 指向虚拟头;3、cur 和 p 一起先后走,直到 p == null,此时 cur 刚好位于被删除节点的前一位。

public ListNode removeNthFromEnd(ListNode head, int n) {
    if (n <= 0 || head == null) return head;
    ListNode hair = new ListNode(0, head), cur = hair, p = head;
    while (n-- > 0) p = p.next;
    while (p != null) {
        cur = cur.next;
        p = p.next;
    }
    cur.next = cur.next.next;
    return hair.next;
}
83. 删除排序链表中的重复元素

在这里插入图片描述

public ListNode deleteDuplicates(ListNode head) {
    ListNode cur = head;
    while (cur != null && cur.next != null) {
        if (cur.val == cur.next.val) cur.next = cur.next.next;
        else cur = cur.next;
    }
    return head;
}
82. 删除排序链表中的重复元素 II
public ListNode deleteDuplicates(ListNode head) {
    ListNode hair = new ListNode(0, head), cur = head, p = hair;
    while (cur != null) {
        while (cur.next != null && cur.val == cur.next.val) cur = cur.next;
        if (p.next == cur) p = p.next;
        else p.next = cur.next;
        cur = cur.next;
    }
    return hair.next;
}

线程池与任务队列

课堂笔记

队列的基础知识
  • 队列是连续的存储区,可以存储一系列的元素,是FIFO结构
    队列通常具有头尾指针(左闭右开),头指针指向第一个元素,尾指针指向最后一个元素的下一位
  • 队列支持(从队尾)入队(enqueue)、(从队首)出队(dequeue)的操作
  • 循环队列可以通过取模操作更加充分利用空间
队列的典型应用场景
  • CPU的超线程技术
  • 线程池的任务队列

LeetCode算法题

86. 分隔链表

在这里插入图片描述

public ListNode partition(ListNode head, int x) {
    ListNode small = new ListNode(), big = new ListNode(), sp = small, bp = big, cur = head;
    while (cur != null) {
        if (cur.val < x) {
            sp.next = cur;
            sp = sp.next;
        } else {
            bp.next = cur;
            bp = bp.next;
        }
        cur = cur.next;
    }
    bp.next = null;
    sp.next = big.next;
    return small.next;
}
138. 复制带随机指针的链表

【思路】难点在于复制随机指针。将原本的 A → B → C 复制成 A → A’ → B → B‘ → C → C’。然后将复制节点中的随机指针域向后推进一格,这样复制节点的随机指针域就指向了随机指针的复制节点。最后将复制的节点拆下来即可。
在这里插入图片描述

public Node copyRandomList(Node head) {
    if (head == null) return null;
    Node cur = head, copy, newHead, next;
    while (cur != null) {
        copy = new Node(cur.val);
        copy.random = cur.random;
        copy.next = cur.next;
        cur.next = copy;
        cur = copy.next;
    }
    cur = head.next;
    while (cur != null) {
        if (cur.random != null) cur.random = cur.random.next;
        cur = cur.next;
        if (cur != null) cur = cur.next;
    }
    newHead = head.next;
    cur = head;
    while (cur != null) {
        next = cur.next;
        if (cur.next != null) cur.next = cur.next.next;
        cur = next;
    }
    return newHead;
}
622. 设计循环队列

【思路】循环队列:尾部入队,头部出队。
在这里插入图片描述

class MyCircularQueue {
    private int[] arr;
    private int front;
    private int rear;
    private int cnt;

    public MyCircularQueue(int k) {
        arr = new int[k];
        front = 0;
        rear = 0;
        cnt = 0;
    }
    
    public boolean enQueue(int value) {
        if (isFull()) return false;
        arr[rear] = value;
        rear = (rear + 1) % arr.length;
        cnt++;
        return true;
    }
    
    public boolean deQueue() {
        if (isEmpty()) return false;
        front = (front + 1) % arr.length;
        cnt--;
        return true;
    }
    
    public int Front() {
        if (isEmpty()) return -1;
        return arr[front];
    }
    
    public int Rear() {
        if (isEmpty()) return -1;
        return arr[(rear - 1 + arr.length) % arr.length];
    }
    
    public boolean isEmpty() {
        return cnt == 0;
    }
    
    public boolean isFull() {
        return cnt == arr.length;
    }
}
641. 设计循环双端队列

在这里插入图片描述

class MyCircularDeque {
    private int[] arr;
    private int front;
    private int rear;
    private int cnt;

    /** Initialize your data structure here. Set the size of the deque to be k. */
    public MyCircularDeque(int k) {
        arr = new int[k];
        front = 0;
        rear = 0;
        cnt = 0;
    }
    
    /** Adds an item at the front of Deque. Return true if the operation is successful. */
    public boolean insertFront(int value) {
        if (isFull()) return false;
        front = (front - 1 + arr.length) % arr.length;
        arr[front] = value;
        cnt++;
        return true;
    }
    
    /** Adds an item at the rear of Deque. Return true if the operation is successful. */
    public boolean insertLast(int value) {
        if (isFull()) return false;
        arr[rear] = value;
        rear = (rear + 1) % arr.length;
        cnt++;
        return true;
    }
    
    /** Deletes an item from the front of Deque. Return true if the operation is successful. */
    public boolean deleteFront() {
        if (isEmpty()) return false;
        front = (front + 1) % arr.length;
        cnt--;
        return true;
    }
    
    /** Deletes an item from the rear of Deque. Return true if the operation is successful. */
    public boolean deleteLast() {
        if (isEmpty()) return false;
        rear = (rear - 1 + arr.length) % arr.length;
        cnt--;
        return true;
    }
    
    /** Get the front item from the deque. */
    public int getFront() {
        if (isEmpty()) return -1;
        return arr[front];
    }
    
    /** Get the last item from the deque. */
    public int getRear() {
        if (isEmpty()) return -1;
        return arr[(rear - 1 + arr.length) % arr.length];
    }
    
    /** Checks whether the circular deque is empty or not. */
    public boolean isEmpty() {
        return cnt == 0;
    }
    
    /** Checks whether the circular deque is full or not. */
    public boolean isFull() {
        return cnt == arr.length;
    }
}
1670. 设计前中后队列

【思路】利用两个双端队列实现,双端队列用双向链表实现。双向链表的插入和删除操作:
在这里插入图片描述

class Node {
    int val;
    Node pre;
    Node next;
    public Node() {}
    public Node(int val) { this.val = val; }
    public void insertPre(Node node) {
        node.pre = this.pre;
        node.next = this;
        if (this.pre != null) this.pre.next = node;
        this.pre = node;
    } 
    public void insertNext(Node node) {
        node.pre = this;
        node.next = this.next;
        if (this.next != null) this.next.pre = node;
        this.next = node;
    }
    public void deletePre() {
        if (this.pre == null) return;
        Node node = this.pre;
        this.pre = node.pre;
        if (this.pre != null) this.pre.next = this;
        node.pre = null;
        node.next = null;
    }
    public void deleteNext() {
        if (this.next == null) return;
        Node node = this.next;
        this.next = node.next;
        if (this.next != null) this.next.pre = this;
        node.pre = null;
        node.next = null;
    }
}
class MyDeque {
     Node dummyHead = new Node();
     Node dummyTail = new Node();
     int cnt;
     public MyDeque() {
         dummyHead.next = dummyTail;
         dummyHead.pre = null;
         dummyTail.pre = dummyHead;
         dummyTail.next = null;
         cnt = 0;
     }
     public void pushFront(int val) {
        dummyHead.insertNext(new Node(val));
        cnt++;
     }
     public void pushBack(int val) {
        dummyTail.insertPre(new Node(val));
        cnt++;
     }
     public int popFront() {
        if (isEmpty()) return -1;
        int ret = dummyHead.next.val;
        dummyHead.deleteNext();
        cnt--;
        return ret;
     }
     public int popBack() {
         if (isEmpty()) return -1;
         int ret = dummyTail.pre.val;
         dummyTail.deletePre();
         cnt--;
         return ret;
     }
     public boolean isEmpty() {
         return cnt == 0;
     }
     public int size() {
         return cnt;
     }
}
class FrontMiddleBackQueue {
    private MyDeque left = new MyDeque();
    private MyDeque right = new MyDeque();

    public FrontMiddleBackQueue() {
    }
    
    public void pushFront(int val) {
        left.pushFront(val);
        reblance();
    }
    
    public void pushMiddle(int val) {
        if (left.size() > right.size()) {
            right.pushFront(left.popBack());
        }
        left.pushBack(val);
    }
    
    public void pushBack(int val) {
        right.pushBack(val);
        reblance();
    }
    
    public int popFront() {
        if (isEmpty()) return -1;
        int ret = left.popFront();
        reblance();
        return ret;
    }
    
    public int popMiddle() {
        if (isEmpty()) return -1;
        int ret = left.popBack();
        reblance();
        return ret;
    }

    public int popBack() {
        if (isEmpty()) return -1;
        int ret = right.isEmpty() ? left.popBack() : right.popBack();
        reblance();
        return ret;
    }
    public boolean isEmpty() {
        return left.size() == 0;
    }
    private void reblance() {
        if (left.size() < right.size()) left.pushBack(right.popFront());
        if (left.size() == right.size() + 2) right.pushFront(left.popBack());
    }
}
933. 最近的请求次数

【思路】使用队列对过程进行模拟。不断弹出队首的过期元素,然后返回队列大小即可。

class RecentCounter {
    private Queue<Integer> queue;

    public RecentCounter() {
        queue = new LinkedList<>();
    }
    
    public int ping(int t) {
        queue.offer(t);
        while (t - queue.peek() > 3000) queue.poll();
        return queue.size();
    }
}
面试题 17.09. 第 k 个数

【思路】先考虑一个问题:对于初始状态(1),我们应该怎么获得后续元素呢?
我们可以将 1 直接从数组中弹出,然后将它的 3 倍、5倍、7倍分别加入数组。然后我们选择[3, 5, 7]中的最小值3,将 3 的 3 倍、5倍、7倍 分别加入到数组。
重复上述过程,直到我们求得第 k 个数。
由于我们每次都需要获得数组的最小值,因此能动态维护当前最值的优先队列(即最大/小堆)结构可以对此过程进行优化,此时的时间复杂度是O(nlgn)。

不重不漏证明:
对于不重复的证明是显然的,在推进过程中,每次循环结束后,加入数组中的值一定严格小于指针正在指向的值的倍率,因此数组是严格单调递增的 —— 3个if中,可能有两个是同时成立的,这导致了p3,p5,p7可能同时被推进一格。
对于不遗漏的严格证明,细节较为繁多,在此不做赘述。由于每个元素都是由数组内的元素的倍率生成的,我们可以通过考虑指针推进的过程,来直观得到这一结论。

public int getKthMagicNumber(int k) {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    int p3 = 0, p5 = 0, p7 = 0;
    while (list.size() < k) {
        int ans = 3 * list.get(p3);
        ans = Math.min(ans, 5 * list.get(p5));
        ans = Math.min(ans, 7 * list.get(p7));
        if (3 * list.get(p3) == ans) p3++;
        if (5 * list.get(p5) == ans) p5++;
        if (7 * list.get(p7) == ans) p7++;
        list.add(ans);
    }
    return list.get(k - 1);
}
859. 亲密字符串

在这里插入图片描述

public boolean buddyStrings(String a, String b) {
    if (a.length() != b.length()) return false;
    if (a.equals(b)) return hasRepeat(a);
    int i = 0, j;
    while (a.charAt(i) == b.charAt(i)) ++i;
    j = i + 1;
    while (j < a.length() && a.charAt(j) == b.charAt(j)) ++j;
    if (j == a.length()) return false;
    if (a.charAt(i) != b.charAt(j) || a.charAt(j) != b.charAt(i)) return false;
    j += 1;
    while (j < a.length()) {
        if (a.charAt(j) != b.charAt(j)) return false;
        j += 1;
    }
    return true;
}

private boolean hasRepeat(String a) {
    Set<Character> set = new HashSet<>();
    for (char c : a.toCharArray()) {
        if (!set.add(c)) {
            return true;
        }
    }
    return false;
}
860. 柠檬水找零
public boolean lemonadeChange(int[] bills) {
    int a = 0, b = 0, c = 0; // 分别记录 5、10、20 的数量
    for (int x : bills) {
        if (x == 5) a++;
        else if (x == 10) {
            if (a == 0) return false;
            a--;
            b++;
        } else {
            if (b > 0) {
                if (a == 0) return false;
                a--;
                b--;
                c++;
            } else {
                if (a < 3) return false;
                a -= 3;
                c++;
            }
        }
    }
    return true;
}
969. 煎饼排序

【思路】每次将第 N 大的元素先翻转到第1位,再翻转到第 N 位,这样第 N 位就无需在后续进程中再进行处理,只需要考虑前 N-1 位即可。
由于每个元素只需要 2 次翻转即可归位,因此所需的次数最多只需 2N 次,符合题目需求。
对于这种做法,可行的优化主要有两个:
一是可以去除值为 1 的翻转,二是可以跳过已经在正确位置上的元素。

public List<Integer> pancakeSort(int[] arr) {
    int len = arr.length;
    List<Integer> result = new ArrayList<>();
    if (len < 2) return result;
    int[] idx = new int[len + 1];
    for (int i = 0; i < len; i++) idx[arr[i]] = i;
    for (int i = len; i >= 1; i--) {
        if (i == idx[i] + 1) continue;
        if (idx[i] + 1 != 1) {
            result.add(idx[i] + 1);
            reverse(arr, idx[i] + 1, idx);
        }
        if (i != 1) {
            result.add(i);
            reverse(arr, i, idx);
        }
    }
    return result;
}
private void reverse(int[] arr, int n, int[] idx) {
    for (int i = 0, j = n -1; i < j; i++, j--) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;

        idx[arr[i]] = i;
        idx[arr[j]] = j;
    }
}
621. 任务调度器
public int leastInterval(char[] tasks, int n) {
    int[] counter = new int[26]; // 统计每一种任务出现的次数
    for (char task : tasks) {
        counter[task - 'A'] += 1;
    }
    Arrays.sort(counter);
    int m = 0;
    for (int i = 25; i >= 0 && counter[i] == counter[25]; i--, m++) {}
    return Math.max(tasks.length, (counter[25] -1) * (n + 1) + m);
}

递归与栈(Stack):解决表达式求值

课堂笔记

栈的基础知识
  • 栈是一种“先进后出(FILO)”的数据结构
有效括号
  • 结论1:在任意⼀个位置上,左括号数量 ≥ 右括号数量
  • 结论2:在最后⼀个位置上,左括号数量 == 右括号数量
  • 根据上述两个结论,程序中只需要记录左括号和右括号的数量即可。
  • ⼀对括号可以等价为⼀个完整的事件。左括号可以看作事件的开始、右括号可以看作事件的结束。⽽括号序列可以看作事件与事件之间的完全包含关系。
  • 栈可以处理具有完全包含关系的问题

LeetCode算法题

面试题 03.04. 化栈为队

【思路】利⽤两个栈来实现,⼀个输⼊栈、⼀个输出栈。输⼊栈⽤于读⼊数据。当需要输出元素时,若输出栈为空,则将输⼊栈的所有元素推送到输出栈,然后取栈顶元素;若输出栈⾮空,则输出栈顶即可。

class MyQueue {
    private Stack<Integer> s1;
    private Stack<Integer> s2;

    /** Initialize your data structure here. */
    public MyQueue() {
        s1 = new Stack<>();
        s2 = new Stack<>();
    }
    
    /** Push element x to the back of queue. */
    public void push(int x) {
        s1.push(x);
    }
    
    /** Removes the element from in front of queue and returns that element. */
    public int pop() {
        transfer();
        return s2.pop();
    }

    private void transfer() {
        if (!s2.empty()) return;
        while (!s1.empty()) {
            s2.push(s1.pop());
        }
    }

    /** Get the front element. */
    public int peek() {
        transfer();
        return s2.peek();
    }
    
    /** Returns whether the queue is empty. */
    public boolean empty() {
        return s1.empty() && s2.empty();
    }
}
682. 棒球比赛
public int calPoints(String[] ops) {
    if (ops == null || ops.length == 0) return -1;
    Stack<Integer> s = new Stack<>();
    for (String op : ops) {
        if ("+".equals(op)) {
            int a = s.pop();
            int b = s.peek();
            s.push(a);
            s.push(a + b);
        } else if ("D".equals(op)) s.push(2 * s.peek());    
        else if ("C".equals(op)) s.pop();
        else s.push(Integer.parseInt(op));
    }
    int sum = 0;
    while (!s.empty()) sum += s.pop();
    return sum;
}
844. 比较含退格的字符串
  • 方法一:栈实现
public boolean backspaceCompare(String S, String T) {
    Stack<Character> s = convert(S);
    Stack<Character> t = convert(T);
    if (s.size() != t.size()) return false;
    while (!s.empty()) {
        if (s.pop() != t.pop()) return false;
    }
    return true;
}
private Stack<Character> convert(String s) {
    Stack<Character> stack = new Stack<>();
    for (char c : s.toCharArray()) {
        if (c == '#' && !stack.empty()) stack.pop();
        else if (c != '#') stack.push(c);
    }
    return stack;
}
  • 方法二:StringBuilder实现
public boolean backspaceCompare(String S, String T) {
    return convert(S).equals(convert(T));
}
private String convert(String s) {
    StringBuilder sb = new StringBuilder();
    for (char c : s.toCharArray()) {
        if (c == '#' && sb.length() > 0) sb.deleteCharAt(sb.length() - 1);
        else if (c != '#') sb.append(c);
    }
    return sb.toString();
}
  • 方法三:双指针法
public boolean backspaceCompare(String S, String T) {
    int cntS = 0, cntT = 0;
    for (int i = S.length() - 1, j = T.length() - 1; i >= 0 || j >= 0;) {
        while (i >= 0) {
            if (S.charAt(i) == '#') { cntS++; i--; }
            else {
                if (cntS > 0) { i--; cntS--; }
                else break;
            }
        }
        while (j >= 0) {
            if (T.charAt(j) == '#') { cntT++; j--; } 
            else {
                if (cntT > 0) { j--; cntT--; }
                else break;
            }
        }
        if (i >= 0 && j >= 0) {
            if (S.charAt(i) != T.charAt(j)) return false;
        } else {
            if (i >= 0 || j >= 0) return false;
        }
        i--;
        j--;
    }
    return true;
}
946. 验证栈序列

【思路】被出栈的元素只有两种可能:即将⼊栈的元素 和 当前栈顶的元素。只需要关注出栈序列,分类讨论后模拟即可。

public boolean validateStackSequences(int[] pushed, int[] popped) {
    Stack<Integer> stack = new Stack<>();
    int i = 0;
    for (int n : pushed) {
        stack.push(n);
        while (!stack.empty() && i < popped.length && stack.peek() == popped[i]) {
            stack.pop();
            i++;
        }
    }
    return stack.empty();
}
20. 有效的括号

image.png

private static final Map<Character, Character> MAP = new HashMap<>(4);
static {
    MAP.put(')', '(');
    MAP.put(']', '[');
    MAP.put('}', '{');
}
public boolean isValid(String s) {
    if (s == null || s.length() == 0) return true;
    Stack<Character> stack = new Stack<>();
    for (char c : s.toCharArray()) {
        switch (c) {
            case '(':
            case '[':
            case '{': stack.push(c); break;
            case ')':
            case ']':
            case '}': if (stack.empty() || stack.peek() != MAP.get(c)) return false; stack.pop(); break;
        }
    }
    return stack.empty();
}
1021. 删除最外层的括号

【思路】左括号和右括号差值为0时,代表这⼀串括号序列是独⽴的,可以被单独分解出来。

  • 方法一:
public String removeOuterParentheses(String S) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0, pre = 0, cnt = 0; i < S.length(); i++) {
        if (S.charAt(i) == '(') cnt++;
        else cnt--;
        if (cnt != 0) continue;
        sb.append(S.substring(pre + 1, i));
        pre = i + 1;
    }
    return sb.toString();
}
  • 方法二:
public String removeOuterParentheses(String S) {
    StringBuilder sb = new StringBuilder();
    int level = 0;
    for (char c : S.toCharArray()) {
        if (c == ')') --level;
        if (level >= 1) sb.append(c);
        if (c == '(') ++level;
    }
    return sb.toString();
}
1249. 移除无效的括号

【思路】定义一个set集合,记录需要删除的字符的索引位置。先正向(从左至右)遍历字符串的每一位字符,定义cnt计数,遇到左括号加一,遇到右括号减一,cnt<0表示该位应该被删除,并将i添加到set。再反向(从右至左)遍历字符串的每一位字符,定义cnt计数,遇到右括号加一,遇到左括号减一,cnt<0表示该位应该被删除,并将i添加到set。最后再次遍历字符串的每一位字符,拼接不在set集合中的索引位的字符。

  • 方法一:
public String minRemoveToMakeValid(String s) {
    Set<Integer> idx = new HashSet<>();
    for (int i = 0, cnt = 0; i < s.length(); i++) {
        char c = s.charAt(i);
        if (c == '(') cnt++;
        else if (c == ')') cnt--;
        if (cnt < 0) {
            cnt = 0;
            idx.add(i);
        }
    }
    for (int i = s.length() - 1, cnt = 0; i >= 0; i--) {
        char c = s.charAt(i);
        if (c == '(') cnt++;
        else if (c == ')') cnt--;
        if (cnt > 0) {
            cnt = 0;
            idx.add(i);
        }
    }
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < s.length(); i++) {
        if (idx.add(i)) sb.append(s.charAt(i));
    }
    return sb.toString();
}
  • 方法二:
public String minRemoveToMakeValid(String s) {
    Stack<Character> s1 = new Stack<>();
    int cnt = 0;
    for (char c : s.toCharArray()) {
        if (c == '(') cnt++;
        else if (c == ')') cnt--;
        if (cnt < 0) {
            cnt = 0;
            continue;
        }
        s1.push(c);
    }
    Stack<Character> s2 = new Stack<>();
    cnt = 0;
    while (!s1.empty()) {
        char c = s1.pop();
        if (c == '(') cnt++;
        else if (c == ')') cnt--;
        if (cnt > 0) {
            cnt = 0;
            continue;
        }
        s2.push(c);
    }
    StringBuilder sb = new StringBuilder();
    while (!s2.empty()) {
        sb.append(s2.pop());
    }
    return sb.toString();
}
145. 二叉树的后序遍历
  • 方法一:递归求解——左右根
public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> ans = new ArrayList<>();
    postOrder(root, ans);
    return ans;
}
private void postOrder(TreeNode root, List<Integer> ans) {
    if (root == null) return;
    postOrder(root.left, ans);
    postOrder(root.right, ans);
    ans.add(root.val);
}
  • 方法二:迭代求解——有限状态机

    技巧是使⽤两个栈,⼀个数据栈,⼀个状态栈。将“遍历左⼦树”,“遍历右⼦树”和“访问根节点”三个步骤分别⽤状态码表⽰,枚举状态转移过程,使⽤有限状态⾃动机(FSM, Finite State Machine)的模型来模拟递归过程。

public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    if (root == null) return result;
    Stack<TreeNode> s1 = new Stack<>(); // 数据栈
    Stack<Integer> s2 = new Stack<>(); // 状态栈
    s1.push(root);
    s2.push(0);
    while (!s1.empty()) {
        switch (s2.pop()) {
            case 0:
                s2.push(1);
                if (s1.peek().left != null) {
                    s1.push(s1.peek().left);
                    s2.push(0);
                }
                break;
            case 1:
                s2.push(2);
                if (s1.peek().right != null) {
                    s1.push(s1.peek().right);
                    s2.push(0);
                }
                break;
            case 2:
                result.add(s1.pop().val);
                break;
        }
    }
    return result;
}
331. 验证二叉树的前序序列化

【思路一】每次拆掉⼀个“数字、#、#”的节点(即叶⼦结点),最后树上的全部节点都会被拆光(即只剩⼀个“#”),能拆光的序列就是合法序列。

【思路二】初始状态有⼀个坑。每多增加⼀个数字节点,会在占掉⼀个坑后,产⽣两个坑,每多增加⼀个#,会减少⼀个坑。合法的⼆叉树前序遍历最后会刚好⽤完所有的坑。

  • 方法一:
public boolean isValidSerialization(String preorder) {
    Deque<String> stack = new LinkedList<>();
    for (String s : preorder.split(",")) {
        if (stack.isEmpty() || !"#".equals(stack.peek())) stack.push(s);
        else {
            if (!"#".equals(s)) {
                stack.push(s);
                continue;
            }
            while ("#".equals(stack.peek())) {
                stack.pop();
                if (!stack.isEmpty() && !"#".equals(stack.peek())) stack.pop();
                else {
                    stack.push(s);
                    break;
                }
            }
            stack.push(s);
        }
    }
    return stack.size() == 1 && "#".equals(stack.peek());
}
  • 方法二:
public boolean isValidSerialization(String preorder) {
    List<String> list = new ArrayList<>();
    for (String s : preorder.split(",")) {
        list.add(s);
        int last = list.size() - 1;
        while (list.size() >= 3 && "#".equals(list.get(last)) && "#".equals(list.get(last - 1)) && !"#".equals(list.get(last - 2))) {
            list.remove(last);
            list.remove(last - 1);
            list.remove(last - 2);
            list.add("#");
            last = list.size() - 1;
        }
    }
    return list.size() == 1 && "#".equals(list.get(0));
}
227. 基本计算器 II

【思路一】找到式⼦中优先级最低的运算符,然后递归分治运算两侧的⼦式即可。

【思路二】使⽤操作数栈和操作符栈辅助计算,当操作符栈遇到更低优先级的操作符时,需要将之前更⾼级别的操作符对应的结果计算出来。

  • 对于有括号的情况,左括号相当于提⾼了内部全部运算符的优先级,当遇到右括号的时候需要将匹配的括号间的内容全部计算出来。

  • 可以通过加⼀个特殊操作符的处理技巧,来额外处理结尾的数字。

public int calculate(String s) {
    Stack<Integer> s1 = new Stack<>();      // 操作数栈
    Stack<Character> s2 = new Stack<>();    // 操作符栈
    int num = 0;
    s += "@";
    for (char c : s.toCharArray()) {
        if (c == ' ') continue;
        if (c >= '0' && c <= '9') {
            num = num * 10 + (c - '0');
            continue;
        }
        s1.push(num);
        num = 0;
        while (!s2.empty() && level(c) <= level(s2.peek())) {
            s1.push(calc(s1.pop(), s1.pop(), s2.pop()));
        }
        s2.push(c);
    }
    return s1.pop();
}

private int level(char c) {
    switch (c) {
        case '@': return -1;
        case '+':
        case '-': return 1;
        case '*':
        case '/': return 2;
        default: return 0;
    }
}

private int calc(int b, int a, char c) {
    switch (c) {
        case '+': return a + b;
        case '-': return a - b;
        case '*': return a * b;
        case '/': return a / b;
        default: return 0;
    }
}
636. 函数的独占时间

【思路】本质就是⼀道模拟题,画⼀个线段图能显著地辅助理解。任务开始时进栈,上⼀个任务暂停执⾏;任务完成时出栈,恢复上⼀个任务的执⾏。

public int[] exclusiveTime(int n, List<String> logs) {
    int[] result = new int[n];
    Stack<Integer> stack = new Stack<>();
    int pre = 0;
    for (String log : logs) {
        String[] slices = log.split(":");
        int id = Integer.parseInt(slices[0]);
        String status = slices[1];
        int tickTime = Integer.parseInt(slices[2]);
        if ("start".equals(status)) {
            if (!stack.empty()) result[stack.peek()] += tickTime - pre;
            pre = tickTime;
            stack.push(id);
        } else {
            result[id] += tickTime - pre + 1;
            pre = tickTime + 1;
            stack.pop();
        }
    }
    return result;
}
// 代码简化
public int[] exclusiveTime(int n, List<String> logs) {
    int[] ans = new int[n];
    Deque<Integer> stack = new LinkedList<>();
    int pre = 0;
    for (String log : logs) {
        String[] slices = log.split(":");
        String status = slices[1];
        int tickTime = Integer.parseInt(slices[2]) + ("start".equals(status) ? 0 : 1);
        if (!stack.isEmpty()) ans[stack.peek()] += tickTime - pre;
        pre = tickTime;
        if ("start".equals(status)) stack.push(Integer.parseInt(slices[0]));
        else stack.pop();
    }
    return ans;
}
1124. 表现良好的最长时间段

【思路】把表现“良好”记为1,表现“不好”记为-1,将原序列转化为正负 1 的序列,原问题转化为求转化后序列的最长一段连续子序列,使得子序列的和大于 0。

这里使用“前缀和”的技巧:前缀和数组的第 n 项,是原数组前 n 项的和。

在本题中 ,转化后的数组中的元素只有 -1 和 1,因此前缀和 prefix 的变化一定是连续的,我们记录下前缀和中,每一个前缀和第一次出现的位置,它对应的位置一定是从该前缀和出发的最优解。

我们以 f ( n ) f(n) f(n) 表示 以 n n n 结尾的序列的最大长度, p o s ( n ) pos(n) pos(n) 表示前缀和 n n n 第一次出现的位置。那么:

f ( n ) = f ( n − 1 ) + p o s ( n ) − p o s ( n − 1 ) f(n) = f(n - 1) + pos(n) - pos(n - 1) f(n)=f(n1)+pos(n)pos(n1)

  • 方法一:
public int longestWPI(int[] hours) {
    Map<Integer, Integer> idx = new HashMap<>(); // 记录每一种值第一次出现的位置
    idx.put(0, -1);
    Map<Integer, Integer> f = new HashMap<>();
    f.put(0, 0);
    int cnt = 0, ans = 0; // 前缀和
    for (int i = 0; i < hours.length; i++) {
        if (hours[i] > 8) cnt++;
        else cnt--;
        if (!idx.containsKey(cnt)) {
            idx.put(cnt, i);
            if (!f.containsKey(cnt - 1)) f.put(cnt, 0);
            else f.put(cnt, f.get(cnt - 1) + (i - idx.get(cnt - 1)));
        }
        if (!idx.containsKey(cnt - 1)) continue;
        ans = Math.max(ans, f.get(cnt - 1) + (i - idx.get(cnt - 1)));
    }
    return ans;
}
  • 方法二:
public int longestWPI(int[] hours) {
    int n = hours.length, ans = 0;
    int[] sums = new int[n + 1]; // 定义前缀和数组
    for (int i = 0; i < n; i++) sums[i + 1] = sums[i] + (hours[i] > 8 ? 1 : -1);
    Map<Integer, Integer> sum2idx = new HashMap<>();
    for (int i = 1; i <= n; i++) {
        int sum = sums[i];
        if (sum > 0) ans = i;
        else {
            if (!sum2idx.containsKey(sum)) sum2idx.put(sum, i);
            if (sum2idx.containsKey(sum - 1)) ans = Math.max(ans, i - sum2idx.get(sum - 1));
        }
    }
    return ans;
}

第四周

本月刷题测试题

面试题 02.02. 返回倒数第 k 个节点
public int kthToLast(ListNode head, int k) {
    if (k <= 0 || head == null) return -1;
    ListNode p = head, cur = head;
    while(k-- > 0 && cur != null) cur = cur.next;
    if (k >= 0) return -1;
    while(cur != null) {
        p = p.next;
        cur = cur.next;
    }
    return p.val;
}
剑指 Offer 22. 链表中倒数第k个节点
public ListNode getKthFromEnd(ListNode head, int k) {
    if (head == null || k <= 0) return null;
    ListNode p = head, cur = head;
    while(k-- > 0 && cur != null) cur = cur.next;
    if (k >= 0) return null;
    while (cur != null) {
        p = p.next;
        cur = cur.next;
    }
    return p;
}
剑指 Offer 35. 复杂链表的复制
public Node copyRandomList(Node head) {
    if (head == null) return null;
    Node p = head, newHead, next;
    while (p != null) {
        Node q = new Node(p.val);
        q.next = p.next;
        q.random = p.random;
        p.next = q;
        p = q.next;
    }
    p = head.next;
    while (p != null) {
        if (p.random != null) p.random = p.random.next;
        p = p.next;
        if (p != null) p = p.next;
    }
    newHead = head.next;
    p = head;
    while (p != null) {
        next = p.next;
        if (p.next != null) p.next = p.next.next;
        p = next;
    }
    return newHead;
}
面试题 02.03. 删除中间节点
public void deleteNode(ListNode node) {
    node.val = node.next.val;
    node.next = node.next.next;
}
445. 两数相加 II

【思路】栈操作,反转链表操作

  • 方法一:使用两个栈,先将两个链表中的元素分别入栈,然后在依次出栈时计算结果。
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    if (l1 == null) return l2;
    if (l2 == null) return l1;
    Stack<Integer> s1 = new Stack<>();
    Stack<Integer> s2 = new Stack<>();
    ListNode p = l1, q = l2;
    while (p != null) {
        s1.push(p.val);
        p = p.next;
    }
    while (q != null) {
        s2.push(q.val);
        q = q.next;
    }
    ListNode hair = new ListNode(1);
    int x = 0, y = 0;
    while (!s1.empty() || !s2.empty()) {
        x = (s1.empty() ? 0 : s1.pop()) + (s2.empty() ? 0 : s2.pop()) + y;
        y = x / 10;
        ListNode node = new ListNode(x % 10);
        node.next = hair.next;
        hair.next = node;
    }
    return y == 1 ? hair : hair.next;
}
  • 方法二:利用链表的反转操作,先将两个链表都反转过来,进行计算后得到新的链表,新的链表再次反转得到答案。
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    ListNode hair = new ListNode(), cur = hair, p = reverse(l1), q = reverse(l2);
    int flag = 0;
    while (p != null || q != null) {
        int sum = (p != null ? p.val : 0) + (q != null ? q.val : 0) + flag;
        flag = sum / 10;
        cur.next = new ListNode(sum % 10);
        cur = cur.next;
        if (p != null) p = p.next;
        if (q != null) q = q.next;
    }
    if (flag == 1) cur.next = new ListNode(1);
    return reverse(hair.next);
}
private ListNode reverse(ListNode head) {
    ListNode pre = null, cur = head, next;
    while (cur != null) {
        next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
}
143. 重排链表

【思路】利用快慢指针找到链表的中间位置,并分割成左右两部分链表,右边部分的链表进行反转操作,最后合并左右两部分链表

  • 方法一:
public void reorderList(ListNode head) {
    ListNode slow = head, fast = head, pre = null;
    while (fast != null && fast.next != null) {
        pre = slow;
        slow = slow.next;
        fast = fast.next.next;
    }
    ListNode left = head, right, next;
    if (fast == null) {
        right = reverse(pre.next);
        pre.next = null;
    } else {
        right = reverse(slow.next);
        slow.next = null;
    }
    while (left != null) {
        next = left.next;
        left.next = right;
        left = next;
        if (right != null) {
            next = right.next;
            right.next = left;
            right = next;
        }
    }
}
private ListNode reverse(ListNode head) {
    ListNode pre = null, cur = head, next;
    while (cur != null) {
        next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
}
  • 方法二:
public void reorderList(ListNode head) {
    if (head == null) return;
    int len = 0;
    ListNode cur = head, l1 = head, l2;
    while (cur != null) {
        cur = cur.next;
        len++;
    }
    if (len <= 2) return;

    cur = l1;
    for (int i = 1; i < (len + 1)/2; i++) cur = cur.next;
    l2 = reverse(cur.next);
    cur.next = null;

    ListNode p = l1, q = l2, next = null;
    while (p != null || q != null) {
        next = p.next;
        p.next = q;
        p = q;
        q = next;
    }
}
private ListNode reverse(ListNode head) {
    ListNode hair = null, cur = head, next;
    while (cur != null) {
        next = cur.next;
        cur.next = hair;
        hair = cur;
        cur = next;
    }
    return hair;
}
  • 方法三:
public void reorderList(ListNode head) {
    if (head == null) return;
    List<ListNode> list = new ArrayList<>();
    ListNode cur = head, pre;
    while (cur != null) {
        pre = cur;
        cur = cur.next;
        pre.next = null;
        list.add(pre);
    }
    for (int i = 0, j = list.size() - 1; i < j;) {
        list.get(i).next = list.get(j);
        i++;
        if (i == j) {
            list.get(i).next = null;
            break;
        }
        list.get(j).next = list.get(i);
        j--;
    }
}
面试题 02.08. 环路检测
public ListNode detectCycle(ListNode head) {
    Set<ListNode> set = new HashSet<>();
    ListNode cur = head;
    while (cur != null) {
        if (!set.add(cur)) {
            return cur;
        }
        cur = cur.next;
    }
    return null;
}
707. 设计链表
class MyLinkedList {
    class ListNode {
        int val;
        ListNode next;
        ListNode pre;
        ListNode() {}
        ListNode(int val) {
            this.val = val;
        }
    }
    private ListNode hair;
    private ListNode tail;
    private int size;
    /** Initialize your data structure here. */
    public MyLinkedList() {
        hair = new ListNode();
        tail = new ListNode();
        hair.next = tail;
        tail.pre = hair;
        size = 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 < 0 || index >= size) return -1;
        ListNode cur = hair;
        for (int i = 0; i <= index; i++) cur = cur.next;
        return cur.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) {
        ListNode node = new ListNode(val);
        node.next = hair.next;
        node.pre = hair;
        hair.next.pre = node;
        hair.next = node;
        size++;
    }
    
    /** Append a node of value val to the last element of the linked list. */
    public void addAtTail(int val) {
        ListNode node = new ListNode(val);
        node.pre = tail.pre;
        node.next = tail;
        tail.pre.next = node;
        tail.pre = node;
        size++;
    }
    
    /** 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 == size) {
            addAtTail(val);
            return;
        }
        if (index > size) return;
        if (index < 0) {
            addAtHead(val);
            return;
        }
        ListNode cur = hair;
        for (int i = 0; i < index; i++) cur = cur.next;
        ListNode node = new ListNode(val);
        node.next = cur.next;
        node.pre = cur;
        cur.next.pre = node;
        cur.next = node;
        size++;
    }
    
    /** Delete the index-th node in the linked list, if the index is valid. */
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) return;
        ListNode cur = hair;
        for (int i = 0; i < index; i++) cur = cur.next;
        cur.next = cur.next.next;
        cur.next.pre = cur;
        size--;
    }
}
剑指 Offer 18. 删除链表的节点
public ListNode deleteNode(ListNode head, int val) {
    ListNode hair = new ListNode(0), cur = hair;
    hair.next = head;
    while (cur.next != null) {
        if (cur.next.val == val) {
            cur.next = cur.next.next;
            break;
        }
        cur = cur.next;
    }
    return hair.next;
}
725. 分隔链表
public ListNode[] splitListToParts(ListNode root, int k) {
    ListNode[] ans = new ListNode[k];
    ListNode cur = root, p = root;
    int len = 0;
    while (cur != null) { len++; cur = cur.next; }
    cur = root;
    int n = len / k, m = len % k;
    for (int i = 0; i < k; i++) {
        ans[i] = p;
        for (int j = 1; j < n && cur != null; j++) cur = cur.next;
        if (cur != null && m-- > 0 && n > 0) cur = cur.next;
        if (cur != null) { p = cur.next; cur.next = null; }
        cur = p;
    }
    return ans;
}
面试题 02.04. 分割链表
public ListNode partition(ListNode head, int x) {
    ListNode small = new ListNode(0), big = new ListNode(0);
    ListNode sp = small, bp = big, cur = head;
    while (cur != null) {
        if (cur.val < x) {
            sp.next = cur;
            sp = sp.next;
        } else {
            bp.next = cur;
            bp = bp.next;
        }
        cur = cur.next;
    }
    bp.next = null;
    sp.next = big.next;
    return small.next;
}
779. 第K个语法符号
public int kthGrammar(int N, int K) {
    if (N == 0) return 0;
    if (K % 2 == 1) return kthGrammar(N - 1, (K + 1) / 2);
    else return Math.abs(kthGrammar(N - 1, K / 2) - 1);
}
剑指 Offer 10- I. 斐波那契数列
  • 方法一:
public int fib(int n) {
    int a = 0, b = 1, sum;
    for (int i = 0; i < n; i++) {
        sum = (a + b) % 1000000007;
        a = b;
        b = sum;
    }
    return a;
}
  • 方法二:
Map<Integer, Integer> map = new HashMap<>();
public int fib(int n) {
    if (n < 2) return n;
    Integer f = map.get(n);
    if (f != null) return f;
    int ans = (fib(n - 1) + fib(n - 2)) % 1000000007;
    map.put(n, ans);
    return ans;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

讲文明的喜羊羊拒绝pua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值