跳转至

Algorithm

约 1 个字 预计阅读时间不到 1 分钟

最长回文子串

约 623 个字 38 行代码 1 张图片 预计阅读时间 3 分钟

题目链接: 5. 最长回文子串

题解链接:5. 最长回文子串 - 力扣(LeetCode)

中心扩散法

从每一个位置出发,向两边扩散即可。遇到不是回文的时候结束。举个例子,str=acdbbdaa 我们需要寻找从第一个 b(位置为 3)出发最长回文串为多少。怎么寻找?

  1. 首先往左寻找与当期位置相同的字符,直到遇到不相等为止。
  2. 然后往右寻找与当期位置相同的字符,直到遇到不相等为止。
  3. 最后左右双向扩散,直到左和右不相等。如下图所示:

image.png

每个位置向两边扩散都会出现一个窗口大小(cur_len)。如果 cur_len > length(用来表示最长回文串的长度)。则更新 length 的值。

因为我们最后要返回的是具体子串,而不是长度,因此,还需要记录一下 length 时的起始位置(start),即此时还要 start = left。

Python
class Solution:
    def longestPalindrome(self, s: str) -> str:
        if s is None or len(s) == 0:
            return ''
        n = len(s)
        start = length = 0
        for i in range(n):
            left, right, cur_len = i - 1, i + 1, 1
            while left >= 0 and s[left] == s[i]:
                left -= 1
                cur_len += 1
            while right < n and s[right] == s[i]:
                right += 1
                cur_len += 1
            while left >= 0 and right < n and s[left] == s[right]:
                left -= 1
                right += 1
                cur_len += 2
            if cur_len > length:
                length = cur_len
                start = left + 1
        return s[start:start+length]

动态规划

中心扩散的方法,其实做了很多重复计算。动态规划就是为了减少重复计算的问题。动态规划听起来很高大上。其实说白了就是空间换时间,将计算结果暂存起来,避免重复计算。作用和工程中用 redis 做缓存有异曲同工之妙。

我们用一个 boolean dp[l][r] 表示字符串从 i 到 j 这段是否为回文。

试想如果 dp[l][r]=true,我们要判断 dp[l-1][r+1] 是否为回文,只需要判断字符串在 (l-1) 和(r+1) 两个位置是否为相同的字符。

进入正题,动态规划关键是找到初始状态和状态转移方程。

初始状态,l=r 时,此时 dp[l][r]=true。

状态转移方程,dp[l][r]=true 并且 (l-1) 和(r+1) 两个位置为相同的字符,此时 dp[l-1][r+1]=true。

Python
class Solution:
    def longestPalindrome(self, s: str) -> str:
        if s is None or len(s) == 0:
            return ''
        n = len(s)
        start = end = 0
        length = 1
        dp = [[False] * n for _ in range(n)]
        for r in range(n):
            for l in range(r):
                if s[l] == s[r] and (r - l <= 2 or dp[l + 1][r - 1]):
                    dp[l][r] = True
                    if r - l + 1 > length:
                        length = r - l + 1
                        start, end = l, r
        return s[start:end+1]

if s[l] == s[r] and (r - l <= 2 or dp[l + 1][r - 1]): 这个判断条件的含义是

1. s.charAt(l) == s.charAt(r)

这一部分表示:当前字符串左右两端的字符是否相等

  • 如果 s[l] 和 s[r] 不相等,那么从 l 到 r 之间的子串显然不可能是回文串,因此直接跳过后续判断。

  • 如果相等,就进一步判断该子串中间部分是否是回文,从而确定整个子串是否是回文串。

2. (r - l <= 2 || dp[l + 1][r - 1])

这一部分表示:在两种情况下,从位置 l 到 r 的子串可以被确认是回文串

(1) r - l <= 2

如果 r - l <= 2,说明 l 和 r 之间最多只有 0 个或 1 个字符,这种情况下:

  • s[l] == s[r] 就足以判断该子串是回文串了,因为中间的部分要么为空(r - l = 1),要么只有一个字符(r - l = 2)。

  • 示例:对于 aba、aa,只需要两端相等即可确认它们是回文。

(2) dp[l + 1][r - 1]

如果 r - l > 2,说明 l 和 r 之间至少隔着两个字符,那么子串 [l+1, r-1] 是否是回文就变得重要。

  • dp[l + 1][r - 1] 是一个动态规划数组,记录从位置 l+1 到 r-1 的子串是否是回文。

  • 如果 dp[l + 1][r - 1] == true,说明该子串是回文串,再加上 s[l] == s[r],可以确认 [l, r] 也是回文串。

  • 如果 dp[l + 1][r - 1] == false,那么中间部分不是回文,即使两端字符相等,也无法形成回文串。

手撕快速排序

约 267 个字 35 行代码 2 张图片 预计阅读时间 1 分钟

题目连接: 912. 排序数组

代码 (三路快排)

Python
class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        self.quick_sort(nums, 0, len(nums) - 1)
        return nums

    def quick_sort(self, nums: List[int], left: int, right: int) -> None:
        if left >= right:
            return
        le, gt = self.partition(nums, left, right)
        self.quick_sort(nums, left, le - 1)
        self.quick_sort(nums, gt, right)

    def partition(self, nums: List[int], left: int, right: int):
        random_index = random.randint(left, right)
        nums[random_index], nums[left] = nums[left], nums[random_index]
        pivot = nums[left]

        # 循环不变量:
        # [left, le - 1] < pivot
        # [le, i] = pivot
        # [gt, right] > pivot
        le = left
        gt = right + 1
        i = left + 1
        while i < gt:
            if nums[i] < pivot:
                nums[i], nums[le] = nums[le], nums[i]
                le += 1
                i += 1
            elif nums[i] == pivot:
                i += 1
            else:
                gt -= 1
                nums[i], nums[gt] = nums[gt], nums[i]
        return le, gt

三路快排

思想:把整个数组分成三个部分,引入两个指针,le (less equal) 和 gt (great than)

image.png

此时有:

  1. nums[left, le - 1] 均小于 V
  2. nums[gt, right] 均大于 V

初始值:

  • le = left
  • gt = right + 1

遍历指针 i 从 left + 1 开始

  1. 遇到比 V 小的,交换 (le, i),le、i 均后移一位
  2. 遇到等于 V 的,不用交换,i 后移一位
  3. 遇到比 V 大的,gt 前移一位,交换 (gt, i)
    • 注意:此时 i 不能后移,因为交换过来的 nums[gt] 之前未看过

现在考虑退出循坏的条件,i 碰到 gt 即可

不要取 "=",因为 i 没看过,表示下一个要看,gt 的值都看过了,故 while i < gt

循环停止时

image.png

此时有:

  1. nums[left, le - 1] 均小于 V
  2. nums[gt, right] 均大于 V
  3. nums[le, gt - 1] 均等于 V

返回 le, gt, 并回到 quick_sort 函数

  • 递归调用左区间 [left, le - 1]
  • 递归调用右区间 [gt, right]
  • 直到排序完成

K 个一组翻转链表

约 138 个字 36 行代码 预计阅读时间 1 分钟

题目链接: 25. K 个一组翻转链表

Python
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        '''
        pre 是这次翻转后的头节点
        p0 是上一次翻转的尾节点
        nxt = p0.next 是这次翻转前的头节点/这次翻转后的尾节点
        cur 是下一次翻转前的头节点
        在一次翻转完成之后
        nxt.next = cur 这一次翻转的尾节点应指向下一次的头节点
        p0.next = pre 上次翻转的尾节点应指向这次翻转的头节点
        p0 变为这次翻转后的尾节点
        '''
        n = 0
        cur = head
        while cur:
            cur = cur.next
            n += 1
        p0 = sentinel = ListNode(next=head)
        pre, cur = None, head
        while n >= k:
            n -= k
            for _ in range(k):  
                nxt = cur.next
                cur.next = pre
                pre = cur
                cur = nxt
            nxt = p0.next
            nxt.next = cur
            p0.next = pre
            p0 = nxt
        return sentinel.next

pre 是这次翻转后的头节点

p0 是上一次翻转的尾节点

nxt = p0.next 是这次翻转前的头节点/这次翻转后的尾节点

cur 是下一次翻转前的头节点


在一次翻转完成之后

nxt.next = cur 这一次翻转的尾节点应指向下一次的头节点

p0.next = pre 上次翻转的尾节点应指向这次翻转的头节点

p0 变为这次翻转后的尾节点

Dijkstra算法

约 94 个字 64 行代码 1 张图片 预计阅读时间 1 分钟

Pasted image 20240717033502

技巧: 如果图为无向图(有向图反转)且不存在负边,要求求各个点 i 到同一终点 n 的最短路径,可以设置Dijkstra的起点和终点 均为 n,求解得到的 distance 数组即 各个点 i 到终点 n 的最短距离。

743. 网络延迟时间为例

Dijkstra算法模版

Java 题解:

Java
class Solution {
    public int networkDelayTime(int[][] times, int n, int s) {
        ArrayList<ArrayList<int[]>> graph = new ArrayList<>();
        // 节点下标为 1 - n
        for (int i = 0; i <= n; i++) {
            graph.add(new ArrayList<>());
        }
        for (int[] edge : times) {
            graph.get(edge[0]).add(new int[]{edge[1], edge[2]});
        }
        int[] distance = new int[n + 1];
        Arrays.fill(distance, Integer.MAX_VALUE);
        distance[s] = 0;
        boolean[] visted = new boolean[n + 1];
        PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> a[1] - b[1]);
        heap.add(new int[]{s, 0});
        while (!heap.isEmpty()) {
            int u = heap.poll()[0];
            if (visted[u]) {
                continue;
            }
            visted[u] = true;
            for (int[] edge : graph.get(u)) {
                int v = edge[0];
                int w = edge[1];
                if (!visted[v] && distance[u] + w < distance[v]) {
                    distance[v] = distance[u] + w;
                    heap.add(new int[] {v, distance[u] + w});
                }
            }
        }
        int ans = Integer.MIN_VALUE;
        for (int i = 1; i <= n; i++) {
            if (distance[i] == Integer.MAX_VALUE) {
                return -1;
            }
            ans = Math.max(ans, distance[i]);
        }
        return ans;
    }
}

Python 题解:

Python
class Solution:
    def networkDelayTime(self, times: List[List[int]], n: int, s: int) -> int:
        graph = [[] for _ in range(n + 1)]
        for u, v, w in times:
            graph[u].append((v, w))

        distance = [inf for _ in range(n + 1)]
        distance[s] = 0
        visited = [False for _ in range(n + 1)]

        heap = [(0, s)]
        while heap:
            _, u = heappop(heap)
            if visited[u]:
                continue
            visited[u] = True
            for v, w in graph[u]:
                if not visited[v] and distance[u] + w < distance[v]:
                    distance[v] = distance[u] + w
                    heappush(heap, (distance[v], v))
        ans = max(distance[1:])
        return ans if ans < inf else -1

滑动窗口模版

约 21 个字 36 行代码 预计阅读时间 1 分钟

统一模版

Java
1
2
3
4
5
6
7
8
//外层循环扩展右边界,内层循环扩展左边界
for (int l = 0, r = 0 ; r < n ; r++) {
        //当前考虑的元素
        while (l <= r && check()) {//区间[left,right]不符合题意
        //扩展左边界
    }
    //区间[left,right]符合题意,统计相关信息
}



模版题:3. 无重复字符的子串

Java题解

Java
class Solution {
    public int lengthOfLongestSubstring(String s) {
        char[] ss = s.toCharArray();
        Set<Character> set = new HashSet<>();
        int res = 0;
        for (int left = 0, right = 0; right < s.length(); right++) {
            char ch = ss[right];
            while (set.contains(ch)) {
                set.remove(ss[left]);
                left++;
            }
            set.add(ss[right]);
            res = Math.max(res, right - left + 1);
        }
        return res;
    }
}

python题解

Python
class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        x = set()
        res, left = 0, 0
        for right in range(len(s)):
            while s[right] in x:
                x.remove(s[left])
                left += 1
            x.add(s[right])
            res = max(res, right - left + 1)
        return res