剑指offer全书题解 (Python)【更新完毕】

文章目录

    • 2 实现 Singleton 模式
    • 3 找出数组中重复的数字
    • 3.2 不修改数组找出重复的数字
    • 4 二维数组中的查找
    • 5 替换空格
    • 6 从尾到头打印链表
    • 7 重建二叉树
    • 8 二叉树的下一个节点
    • 9 用两个栈实现队列
    • 9.1 用两个队列实现一个栈
    • 10 斐波那契数列
    • 10.2 青蛙跳台阶
    • 10.3 青蛙变态跳台阶
    • 10.4 矩形覆盖问题
    • 11 旋转数组的最小数字
    • 12 矩阵中的路径
    • 13 机器人运动的范围
    • 14 剪绳子
    • 15 二进制中 1 的个数
    • 16 数值的整数次方
    • 17 打印从 1 到最大的 n 位数
    • 18 在O(1)时间删除链表节点
    • 18.2 删除排序链表中重复的节点
    • 19 正则表达式匹配
    • 20 表示数值的字符串
    • 21 调整数组顺序使奇数位于偶数前
    • 22 链表中倒数第 K 个节点
    • 23 链表中环的入口节点
    • 24 反转链表
    • 25 合并两个有序链表
    • 26 树的子结构
    • 27 二叉树的镜像
    • 28 对称的二叉树
    • 29 顺时针打印矩阵
    • 30 包含min函数的栈
    • 31 栈的压入、弹出序列
    • 32 从上往下打印二叉树
    • 32.1 分行从上到下打印二叉树
    • 32.2 之字形打印二叉树
    • 33 二叉搜索树的后序遍历
    • 34 二叉树中和为某一值的路径
    • 35 复杂链表的复制
    • 36 二叉搜索树与双向链表
    • 37 序列化二叉树
    • 38 字符串的排列
    • 38.2 字符串的所有组合
    • 38.3 八皇后问题
    • 39 数组中出现次数超过一半的数字
    • 40 最小的 k 个数
    • 41 数据流中的中位数
    • 42 连续子数组的最大和
    • 43 1 ~ n 整数中 1 出现的次数
    • 44 数字序列中某一位的数字
    • 45 把数组排成最小的数
    • 46 把数字翻译成字符串
    • 47 礼物的最大价值
    • 48 最长不含重复字符的子字符串
    • 49 丑数
    • 50.1 第一个只出现一次的字符
    • 50.2 字符流中第一个只出现一次的字符
    • 51 数组中的逆序对
    • 52 两个链表的第一个公共节点
    • 53.1 数字在排序数组中出现的次数
    • 53.2 0~n-1 中缺失的数字
    • 53.3 数组中数值与下标相等的元素
    • 54 二叉搜索树的第k小的节点
    • 55.1 二叉树的深度
    • 55.2 平衡二叉树
    • 56.1 数组中只出现一次的两个数字
    • 56.2 数组中唯一只出现一次的数字
    • 57.1 和为 s 的数字
    • 57.2 和为 s 的连续正数序列
    • 58.1 翻转字符串
    • 58.2 左旋转字符串
    • 59 滑动窗口的最大值
    • 60 n个骰子的点数
    • 61 扑克牌中的顺子
    • 62 圆圈中最后剩下的数字
    • 63 股票的最大利润
    • 64 求 1+2+...+n
    • 65 不用加减乘除做加法
    • 66 构建乘积数组
    • 67 字符串转化为整数
    • 68 最低公共祖先

2 实现 Singleton 模式

使用 __new__控制实例创建过程

class Singleton:
    _instance = None

    def __init__(self):
        pass

    def __new__(cls, *args, **kw):
        if not cls._instance:
            cls._instance = super().__new__(cls)
        return cls._instance

class MyClass(Singleton):
    pass

3 找出数组中重复的数字

题目描述:

在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,
但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。

例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

解析:

长度为n,数字范围0~n-1,如果这个数组不存在重复的数字,那么当数组排序后数字 i 将出现在下标为 i 的位置。

即下方跳出 while 循环。

  1. 让我们重新在排列这个数组,从头到尾依次扫描每个数字。当扫到下标为 i 的数字,首先判断这个数字(m)是否等于 i。如果是,则扫描下一个数字
  2. 若不是,则再拿它和下标为 m 的数字比较,相等则找到一个重复的数字 (该数字在下标为 i 和 m 的位置都出现了),若不等则交换两者位置。使得数字 m 对应下标 m。
  3. 接着继续重复这个过程,直到找到重复数字为止。
class Solution:
    def duplicate(self, nums, duplication):
        """Space: O(1)
        """
        for i, num in enumerate(nums):
            while i != num:  # 当数字m与下标不相等时
                if nums[num] == num: # 当数字m与第m个数字相等时,就找到了
                    duplication[0] = num
                    return True
                else:  #否则交换
                    nums[i], nums[num] = nums[num], nums[i]
                    num = nums[i]
        return False

    def duplicate_1(self, nums, duplication):
        """Space: O(n)
        另起一个数组存储出现过的字符
        """
        t = []
        for x in nums:
            if x in t:
                duplication[0] = x
                return True
            else:
                t.append(x)
        return False

3.2 不修改数组找出重复的数字

题目:

给定一个长度为 n+1 的数组nums,数组中所有的数均在 1∼n 的范围内,其中 n≥1。
请找出数组中任意一个重复的数,但不能修改输入的数组。
样例 给定 nums = [2, 3, 5, 4, 3, 2, 6, 7]。 返回 2 或 3。如果只能使用 O(1) 的额外空间,该怎么做呢?

解析:

这道题目主要应用了抽屉原理和分治的思想。
抽屉原理:n+1 个苹果放在 n 个抽屉里,那么至少有一个抽屉中会放两个苹果。
用在这个题目中就是,一共有 n+1 个数,每个数的取值范围是1到n,所以至少会有一个数出现两次。
然后我们采用分治的思想,将每个数的取值的区间[1, n]划分成[1, n/2]和[n/2+1, n]两个子区间,然后分别统计两个区间中数的个数。
注意这里的区间是指,数的取值范围,而不是数组下标。
划分之后,左右两个区间里一定至少存在一个区间,区间中数的个数大于区间长度。
这个可以用反证法来说明:如果两个区间中数的个数都小于等于区间长度,那么整个区间中数的个数就小于等于n,和有n+1个数矛盾。
因此我们可以把问题划归到左右两个子区间中的一个,而且由于区间中数的个数大于区间长度,根据抽屉原理,在这个子区间中一定存在某个数出现了两次。
依次类推,每次我们可以把区间长度缩小一半,直到区间长度为1时,我们就找到了答案。
时间复杂度:每次会将区间长度缩小一半,一共会缩小 O(logn) 次。每次统计两个子区间中的数时需要遍历整个数组,时间复杂度是 O(n)。所以总时间复杂度是 O(nlogn)。
空间复杂度:代码中没有用到额外的数组,所以额外的空间复杂度是 O(1)。

但是不保证找出所有的重复数字。

若左边区间数字出现的次数小于范围,并不保证一定不存在重复数字。

class Solution:
    def findDuplicate(self, nums) -> int:
        """O(nlogn)
        不保证找出所有重复数字
        """
        if not nums: return
        l, r = 1, len(nums)-1  # 数值的范围不是下标的范围,所以是1~n 题目给出。
        while l<r:
            mid = l + r >> 1  # [l, mid], [mid+1, r]
            s = 0
            for x in nums:
                # 计算左边区间数字的个数
                if l <= x <= mid:
                    s += 1
            if s > mid - l + 1:  #若左边区间数字出现的次数大于范围,则重复数据一定在此区间
                r = mid
            else: l = mid +  1
        return r

4 二维数组中的查找

题目: leetcode 240

在一个二维数组中,每一行都按照从左到右递增的顺序排序。
每一列都按照从上到下递增的顺序排序。
给定一个整数,查找数组中是否存在该整数。
[ [1,2,8,9],
[2,4,9,12],
[4,7,10,13],
[6,8,11,15] ]

解析:

不能选左上或右下,因为侯选区域分两块了,变得复杂。
所以从右上或者坐下开始搜索,每次只需考虑一种情况。

class Solution(object):
    def searchArray(self, array, target):
        if not array:
            return False
        row, col = 0, len(array[0]) - 1
        while row <= len(array)-1 and col >= 0:
            if array[row][col] == target:
                return True
            elif array[row][col] < target:
                row += 1
            else:
                col -= 1
        return False

5 替换空格

题目:

请实现一个函数,把字符串中的每个空格替换成"%20"。

解析:

  1. 首先遍历一遍原数组,求出最终答案的长度length;
  2. 将原数组resize成length大小;
  3. 使用两个指针,指针i指向原字符串的末尾,指针j指向length的位置;
  4. 两个指针分别从后往前遍历,如果str[i] == ’ ‘,则指针j的位置上依次填充’0’, ‘2’, ‘%’,这样倒着看就是"%20";如果str[i] != ’ ',则指针j的位置上填充该字符即可。
class Solution:
    def replaceSpace(self, s):
        """常规解法
        O(n)
        """
        if not s: return ''
        s = list(s)
        # 求出填充之后的长度
        length = 0
        for x in s:
            if x == ' ':
                length += 3
            else:
                length += 1
        # 扩充原字符串
        i, j = len(s) - 1, length - 1
        s += [0] * (length - len(s))
        while i >= 0:
            if s[i] == ' ':
                s[j] = '0'
                s[j - 1] = '2'
                s[j - 2] = '%'
                j -= 3
            else:
                s[j] = s[i]
                j -= 1
            i -= 1
        return ''.join(s)
    
    def replaceSpace(self, s):
        """pythonic
        """
        if type(s) != str:
            return ''
        return s.replace(' ', '%20')

6 从尾到头打印链表

题目:

输入一个链表的头结点,按照 从尾到头 的顺序返回节点的值。

解析

  1. 遍历+倒序
  2. 递归
class Solution(object):
    def printListReversingly(self, head: ListNode) -> List[int]:
        """遍历+倒序
        """
        if not head: return []
        res = []
        while head:
            res.append(head.val)
            head = head.next
        res.reverse()
        return res

    def printListReversingly_1(self, head):
        """递归
        """
        self.res = []
        self.dfs(head)
        return self.res

    def dfs(self, head):
        if not head:
            return
        self.dfs(head.next)
        self.res.append(head.val)

7 重建二叉树

题目: leetcode 105.

输入某二叉树的前序遍历中序遍历的结果,请重建出该二叉树。

假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

解析:

  1. 找到各个子树的根节点 root
  2. 递归构建该根节点的左子树
  3. 递归构建该根节点的右子树
class Solution:
    def buildTree(self, preorder, inorder):
        """返回根节点
        """
        if not preorder or not inorder:
            return
        # 前序遍历的第一个节点为根节点
        root = TreeNode(preorder[0])
        # 因为没有重复元素,所以可以直接根据值来查找根节点在中序遍历中的位置
        mid = inorder.index(preorder[0])
        # 左子树根节点
        left = self.buildTree(preorder[1:mid+1], inorder[:mid])
        # 右子树根节点
        right = self.buildTree(preorder[mid+1:], inorder[mid+1:])
        root.left = left
        root.right = right
        return root

8 二叉树的下一个节点

题目: 牛客网

给定一棵二叉树的其中一个节点,请找出中序遍历 [左,根,右] 序列的下一个节点。

  • 如果给定的节点是中序遍历序列的最后一个,则返回空节点;
  • 二叉树一定不为空,且给定的节点一定不是空节点;

解析:

  1. 当此节点有右子树,下一个节点就是右子树中最左侧的节点。
  2. 当此节点没有右子树时,
    • 若它是它父节点的左节点,那么下一个节点就是它的父节点
    • 若不是,就沿着父指针向上遍历,直到找到一个是它父节点的左节点,这个父节点就是我们要找的下一个节点。
class Solution:
    def GetNext(self, pNode: TreeLinkNode):
        """中序遍历的下一个
        [left, root, right]
        """
        # pNode 不存在则返回None
        if not pNode: return
        # 节点有右子树,则下一个节点就是它右子树的最左节点
        if pNode.right:
            pRight = pNode.right
            while pRight.left:
                pRight = pRight.left
            return pRight
        # 节点没有右子树,沿着父节点,直到找到是它父节点的左节点
        while pNode.next:
            parent = pNode.next
            if parent.left == pNode:
                return parent
            pNode = parent
        return # 不存在就返回None

9 用两个栈实现队列

题目: leetcode 232

请用栈实现一个队列,支持如下四种操作:

  • push(x) – 将元素x插到队尾;
  • pop() – 将队首的元素弹出,并返回该元素;
  • peek() – 返回队首元素;
  • empty() – 返回队列是否为空;

解析:

  1. push(x):直接将x插入栈1中,时间复杂度O(1)

  2. pop():队列是先进先出,栈是先进后出,所以将栈1所有的元素放入栈2中,此时最先进入的元素在栈2的顶部,弹出即可。下次若栈2不为空,直接弹出栈顶元素即可。时间复杂度O(n)

这种解法是在出队时保证队先进先出的特性。

class MyQueue:

    def __init__(self):
        self.s1 = []
        self.s2 = []

    def push(self, x: int) -> None:
        self.s1.append(x)

    def pop(self) -> int:
        if self.s2:
            return self.s2.pop()
        while self.s1:
            self.s2.append(self.s1.pop())
        return self.s2.pop()

    def peek(self) -> int:
        if self.s2:
            return self.s2[-1]
        while self.s1:
            self.s2.append(self.s1.pop())
        return self.s2[-1]

    def empty(self) -> bool:
        if self.s1 or self.s2:
            return False
        else:
            return True

解法二:

进队时即保证队先进先出的特性。

    def push(self) -> int:
        while self.s1:
            self.s2.append(self.s1.pop())
        self.s1.append(x)
        while self.s2:
            self.s1.append(self.s2.pop())
            
    def pop(self) -> int:
        return self.s1.

9.1 用两个队列实现一个栈

题目: leetcode 225

使用队列实现栈的下列操作:

push(x) – 元素 x 入栈
pop() – 移除栈顶元素
top() – 获取栈顶元素
empty() – 返回栈是否为空

解析:

  1. push的时候保证栈的特性即可,栈是先进候出,队列先进先出,入队时,将队1所有元素放入队2,将元素x入队1,再将队2所有元素入队1,则保证了栈的特性
  2. pop直接返回队1队首元素即可。

class MyStack:

    def __init__(self):
        from collections import deque
        self.q1 = deque()
        self.q2 = deque()

    def push(self, x: int) -> None:
        while self.q1:
            self.q2.append(self.q1.popleft())
        self.q1.append(x)
        while self.q2:
            self.q1.append(self.q2.popleft())

    def pop(self) -> int:
        return self.q1.popleft()

    def top(self) -> int:
        return self.q1[0]

    def empty(self) -> bool:
        if self.q1:
            return False
        return True

10 斐波那契数列

题目:leetcode 209

求斐波那契数列的第n项

解析:
KaTeX parse error: No such environment: equation at position 16: f(n) = \begin{̲e̲q̲u̲a̲t̲i̲o̲n̲}̲ \begin{cases} …

  • 递归 (存在大量重复计算)
  • 递推
class Solution:
    def fib(self, N: int) -> int:
        """O(n), O(1)
        递归+滚动变量
        """
        if N < 2: return N
        f0, f1, fn = 0, 1, 0
        for _ in range(2, N+1):
            fn = f0 + f1
            f0, f1 = f1, fn
        return fn
    
    def fib(self, N: int) -> int:
        """递归 O(2^n)
        """
        if N <= 0:
            return 0
        if N == 1:
            return 1
        return self.fib(N-1) + self.fib(N-2)

另外一种解法:矩阵乘法+快速幂

利用矩阵运算的性质将通项公式变成幂次形式,然后用平方倍增(快速幂)的方法求解第 n 项。

先说通式:
[ a n + 1 a n a n a n − 1 ] = [ 1 1 1 0 ] n \begin{bmatrix} a_{n+1} & a_{n} \\ a_{n} & a_{n-1} \\ \end{bmatrix}= \begin{bmatrix} 1 & 1 \\ 1 & 0 \\ \end{bmatrix}^n [an+1ananan1]=[1110]n

利用数学归纳法证明:
这里的a0,a1,a2是对应斐波那契的第几项
令 A = [ 1 1 1 0 ] , 则 A 1 = [ a 2 a 1 a 1 a 0 ] 显 然 成 立 令A =\begin{bmatrix} 1 & 1 \\ 1 & 0 \\ \end{bmatrix},则A^1 = \begin{bmatrix} a_{2} & a_{1} \\ a_{1} & a_{0} \\ \end{bmatrix} 显然成立 A=[1110]A1=[a2a1a1a0]

A n = A n − 1 × A = [ a n a n − 1 a n − 1 a n − 2 ] × [ a 2 a 1 a 1 a 0 ] = [ a n + 1 a n a n a n − 1 ] A^n = A^{n-1} \times A = \begin{bmatrix} a_{n} & a_{n-1} \\ a_{n-1} & a_{n-2} \\ \end{bmatrix} \times \begin{bmatrix} a_{2} & a_{1} \\ a_{1} & a_{0} \\ \end{bmatrix}= \begin{bmatrix} a_{n+1} & a_{n} \\ a_{n} & a_{n-1} \\ \end{bmatrix} An=An1×A=[anan1an1an2]×[a2a1a1a0]=[an+1ananan1]

证毕。

所以我们想要的得到 a n a_n an ,只需要求得 A n A^n An ,然后取第一行第二个元素即可。

如果只是简单的从0开始循环求n次方,时间复杂度仍然是O(n),并不比前面的快。我们可以考虑乘方的如下性质,即快速幂:
a n = { a n / 2 ⋅ a n / 2 n 为偶数 a ( n − 1 ) / 2 ⋅ a ( n − 1 ) / 2 ⋅ a n 为奇数 a^n= \begin{cases} a^{n/2} \cdot a^{n/2} & \text {n 为偶数} \\ a^{(n-1)/2} \cdot a^{(n-1)/2} \cdot a & \text {n 为奇数} \end{cases} an={an/2an/2a(n1)/2a(n1)/2a为偶数为奇数
这样只需要 logn 次运算即可得到结果,时间复杂度为 O(logn)

def mul(a, b):  # 首先定义二阶矩阵乘法运算
    c = [[0, 0], 
         [0, 0]]  # 定义一个空的二阶矩阵,存储结果
    for i in range(2):  # row
        for j in range(2):  # col
            for k in range(2):  # 新二阶矩阵的值计算
                c[i][j] += a[i][k] * b[k][j]
    return c

def fib(n):
    res = [[1, 0], 
           [0, 1]]  # 单位矩阵,等价于1,作为base
    A = [[1, 1], 
         [1, 0]]  # A矩阵
    while n:
        # 1. 如果n是奇数,则先提取一个A出来
        # 2. 停止条件 n == 1
        if n & 1: res = mul(res, A)
        A = mul(A, A)  # 快速幂
        n >>= 1  # 整除2,向下取整
    return res[0][1]

10.2 青蛙跳台阶

题目: 牛客网

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

解析:

记 n 阶台阶的跳法看成 n 的函数,记为 f(n)

  • 若第一次跳的时候只跳 1 级,那么剩下的 n-1 个台阶的跳法是 f(n - 1)
  • 若第一次跳的时候只跳 1 级,那么剩下的 n-2 个台阶的跳法是 f(n - 2)
  • 可以得出 f(n) = f(n-1) + f(n-2),f(1) = 1, f(2) = 2
class Solution:
    def jumpFloor(self, number):
        if number <=2 : 
            return max(0, number)
        f1, f2, fn = 1, 2, 0
        for _ in range(3, number+1):
            fn = f1 + f2
            f1, f2 = f2, fn
        return fn

10.3 青蛙变态跳台阶

题目:牛客网

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

解析:

每个台阶都有跳与不跳两种情况(除了最后一个台阶),最后一个台阶必须跳。所以共用 2 ( n − 1 ) 2^{(n-1)} 2(n1) 中情况.

class Solution:
    def jumpFloorII(self, number):
        return 2**(number-1)

10.4 矩形覆盖问题

题目: 牛客网

我们可以用2x1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2x1的小矩形无重叠地覆盖一个2xn的大矩形,总共有多少种方法?

解析:

小矩形有两种摆法,横着和竖着,记 2xn 的大矩形的摆法为 f(n)

  1. 第一个小矩形竖着放时,剩余的 2x(n-1) 的矩形的摆法是 f(n-1)
  2. 第一个小矩形横着放时,下面必须再横着放一个小矩形,剩余的 2x(n-2) 的矩形的摆法是 f(n-2)
  3. 故 f(n) = f(n-1) + f(n-2),f(1) = 1, f(2) = 2
class Solution:
    def rectCover(self, n):
        if n<=2:
            return n
        f1, f2, fn = 1, 2, 0
        for _ in range(3, n + 1):
            fn = f1 + f2
            f1, f2 = f2, fn
        return fn

11 旋转数组的最小数字

题目: leetcode 153

假设按照升序排序的数组在预先未知的某个点上进行了旋转

( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,|||,0,1,2] )。

请找出其中最小的元素。你可以假设数组中不存在重复元素。

解析:

二分法

看到有序序列,自然想到二分法。

剑指offer全书题解 (Python)【更新完毕】_第1张图片

由图可以看到,线段由两段递增序列组成,左边大于等于nums[0],右边小于nums[0]。我们要找到右边第一个小于nums[0] 的点。即为我们整个数组的最小值。

class Solution:
    def findMin(self, nums: List[int]) -> int:
        if not nums:
            return 
        n = len(nums) - 1
        if n == 1:  # 单元素自然有序
            return nums[0]
        # 升序则返回第一个,旋转0个或n个时
        if nums[0] < nums[-1]:
            return nums[0]
        # 去除后面与前面重复的部分
        while n>0 and nums[n] == nums[0]: 
            n -= 1 
        # 找到第一个小于nums[0]的数
        l, r = 0, n
        while l < r:
            mid = l + r >> 1
            if nums[mid] >= nums[0]:
                l = mid + 1
            else:
                r = mid
        return nums[l]

12 矩阵中的路径

题目: leetcode 79

给定一个二维网格和一个单词,找出该单词是否存在于网格中。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例:

board = 
[
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]

给定 word = "ABCCED", 返回 true.
给定 word = "SEE", 返回 true.
给定 word = "ABCB", 返回 false.

解析:

回溯法

外层:遍历

首先遍历 board 的所有元素,先找到和 word 第一个字母相同的元素,然后进入递归流程。

内层:递归
给进入的节点打标记。递归流程主要做了这么几件事:

  • 从 (i, j) 出发,朝它的上下左右试探,看看它周边的这四个元素是否能匹配 word 的下一个字母

  • 如果匹配到了:带着该元素继续进入下一个递归

  • 如果都匹配不到:返回 False

  • 当 word 的所有字母都完成匹配后,整个流程返回 True

几个注意点

  • 递归时元素的坐标是否超过边界

  • 标记以及 return 的时机

class Solution:
    def exist(self, m: List[List[str]], word: str) -> bool:
        def dfs(u, i, j):  # u为当前匹配的多少字符,ij为坐标
            # 停止条件
            if u == len(word) - 1:
                return True
            # 标记visit,并保存
            temp, m[i][j] = m[i][j], '*'
            # 四个方向探索
            for a, b in ((i, j + 1), (i + 1, j), (i, j - 1), (i - 1, j)):
                if 0 <= a < len(m) and 0 <= b < len(m[0]) and m[a][b] == word[u + 1] and dfs(u + 1, a, b):
                    return True
            m[i][j] = temp  # 回溯
            return False  # 如果都匹配不到:返回 False

        for i in range(len(m)):
            for j in range(len(m[0])):
                if m[i][j] == word[0] and dfs(0, i, j):
                    return True
        return False

13 机器人运动的范围

题目: 牛客网

地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。

例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?

解析: DFS

没有回溯的步骤,因为一个格子最多进入一次。

  1. 从第一个格子进入递归
  2. 标记已访问,res+=1,在四个方向继续探索, 符合则进入递归
  3. 没有停止条件,搜索完就停止
  4. 是数位之和,不是两数之和
class Solution:
    def movingCount(self, threshold, rows, cols):
        def dfs(i, j):  # i, j为坐标
            visited[i][j] = True
            self.res += 1
            for a, b in ((i - 1, j), (i + 1, j), (i, j - 1), (i, j + 1)):
                if 0 <= a < rows and 0 <= b < cols and not visited[a][b] and self.get_sum(a, b) <= threshold:
                    dfs(a, b)

        if not rows or not cols or threshold < 0: return 0
        self.res = 0
        visited = [[False for _ in range(cols)] for _ in range(rows)]
        dfs(0, 0)
        return self.res

    def get_sum(self, a, b):
        """两数数位之和
        """
        return sum(map(int, str(a) + str(b)))

14 剪绳子

题目:AcWing

给你一根长度为 n 绳子,请把绳子剪成 m 段(m、n 都是整数,2≤n≤58 并 m≥2。每段的绳子的长度记为k[0]、k[1]、……、k[m]。它们可能的最大乘积是多少?

例如当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到最大的乘积18。

解析:

解法一:动态规划

  1. f[n] 为长度为 n 的绳子剪成若干段后乘积的最大值

  2. 剪第一刀时,可选1~n-1,n-1中剪法,我们要选取其中的最大值,
    f [ n ] = m a x ( f [ i ] × f [ n − i ] ) , 1 < = i < = n / 2 f[n] = max(f[i] \times f[n-i]), \qquad 1<=i<=n/2 f[n]=max(f[i]×f[ni]),1<=i<=n/2

注意边界条件

class Solution():
    def maxProductAfterCutting(self, length):
        """动态规划
        """
        if length < 2: return 0   # 长度小于2拆不了
        if length == 2: return 1  # 长度为2只能拆成1+1
        if length == 3: return 2  # 长度为3拆成1+2最大
        f = [-1] * (length + 1) 
        f[0], f[1], f[2], f[3] = 0, 1, 2, 3
        for i in range(4, length + 1):
            maxv = 0
            for j in range(1, i//2 + 1):
                maxv = max(maxv, f[j] * f[i - j])
            f[i] = maxv
        return f[length]

解法二:贪婪算法

当 n >= 5 时,尽可能多地剪长度为 3 的绳子;当剩下长度为 4 时,把绳子剪成两段长度为 2 的绳子。

证明:

首先 n >= 5 时,可证 2(n-2) > n 并且 3(n-3) > n。就是说当绳子剩下长度大于或等于5时,3(n-3) >= 2(n-2) ,因此当 n >= 5 时,尽可能多地剪长度为 3 的绳子。

当长度为 4 时,剪成 2 x 2 最大。所以此时就不用拆 3 了。

    def maxProductAfterCutting0(self, length):
        """贪婪算法
        """
        if length < 2: return 0
        if length == 2: return 1
        if length == 3: return 3
        # 尽可能的剪去长度为3的绳子,可能余0,1,2
        number_3 = length // 3
        # 若最后余1,说明最后可以剩下4,取出4
        if length - number_3 * 3 == 1:
            number_3 -= 1
        # 算出2的个数
        number_2 = (length - number_3 * 3) // 2
        return 2 ** number_2 * 3 ** number_3

15 二进制中 1 的个数

题目: leetcode 191

编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数

解析: 位运算

class Solution(object):
    def NumberOf1(self,n):
        res = 0
        for _ in range(32):  # 防止死循环,负数右移补1
            res += n&1
            n>>=1
        return res

    def f2(self,n):
        """把一个整数减去1,都是把最右边的1变成0,
        再和原整数做与运算,会把该整数最右边的1变成0
        1100 - 1 =
        1011 & = 
        1000
        """
        res = 0
        while n:
            res +=1  # 一个不为0的整数至少含有一个1
            n &= n-1 # 消灭一个 1 
        return res

16 数值的整数次方

题目: leetcode 50

实现 pow(x, n) ,即计算 x 的 n 次幂函数。

解析: 快速幂
a n = { a n / 2 ⋅ a n / 2 n 为偶数 a ( n − 1 ) / 2 ⋅ a ( n − 1 ) / 2 ⋅ a n 为奇数 a^n= \begin{cases} a^{n/2} \cdot a^{n/2} & \text {n 为偶数} \\ a^{(n-1)/2} \cdot a^{(n-1)/2} \cdot a & \text {n 为奇数} \end{cases} an={an/2an/2a(n1)/2a(n1)/2a为偶数为奇数

class Solution(object):
    def myPow(self, x, n):
        if x == 0:
            return 1
        res = 1
        if n < 0: # n<0时 求倒数
            x = 1 / x
            n = -n
        while n:
            if n & 1:  # n为奇数时,先提取一个出来。以及循环停止时,获取答案
                res *= x
            x *= x
            n >>= 1
        return res
    
    def myPow_1(self, x, n):
        """递归
        """
        if x == 0:
            return 0
        if n == 0:
            return 1
        if n == 1:
            return x
        if n < 0:
            x = 1 / x
            n =-n
        res = self.myPow(x, n>>1)
        res *= res
        if n & 1:
            res *= x
        return res

17 打印从 1 到最大的 n 位数

题目:

输入数字 n ,按顺序打印出 1 到 n 的所有整数。

解析:

解法一:字符串模拟加法

  1. 初始化一个全为 0 的 n 位数组

  2. 模拟加法,每次加 1

    • 注意进位

    • 若溢出,返回变量停止循环

  3. 打印数组

    • 注意要从第一个非零字符开始打印
def Print(n):
    if n <= 0:
        return
    nums = ['0'] * n
    while not Add(nums):
        print_arr(nums)

def Add(nums):
    """字符串加一操作
    """
    stop = False  # 溢出的标记
    carry = 0  # 进位
    for i in range(len(nums) - 1, -1, -1):
        sumv = int(nums[i]) + carry
        if i == len(nums) - 1:  # 从最低位开始加起
            sumv += 1
        if sumv >= 10:  # 若大于10,考虑进位
            if i == 0:  # 若是最高位大于10,停止外层while循环
                stop = True
            else:
                sumv -= 10  # 去除进位
                carry += 1  # 进为+1
                nums[i] = str(sumv)
        else:
            nums[i] = str(sumv)  # 直到某一位的和小于10,直接赋值并跳出,没有进位
            break
    return stop

def print_arr(nums):
    """从第一个非零字符开始打印字符串
    """
    flag = False
    for x in nums:
        if x != '0':
            flag = True
        if flag:
            print(x, end='')
    print(end=' ')

解法二:全排列

n 位所有十进制数其实是 n 个从 0 到 9 的全排列。也就是把数字的每一位都从 0 到 9 排列一遍。

def Print2(n):
    """解法二:数字全排列,递归"""
    if n <= 0: return
    num = ['0'] * n
    for i in range(10):
        num[0] = str(i)
        printRecur(num, n, 0)

def printRecur(num, n, idx):
    if idx == n - 1:
        printArray(num)
        return
    for i in range(10):
        num[idx + 1] = str(i)
        printRecur(num, n, idx + 1)

18 在O(1)时间删除链表节点

题目: leetcode 237

请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。

解析:

将下一个节点的值赋给自身,删除下一个节点

class Solution(object):
    def deleteNode(self, node):
        if not node:
            return
        node.val = node.next.val
        node.next = node.next.next

18.2 删除排序链表中重复的节点

题目: 牛客网 leetcode 82

给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。

输入: 1->2->3->3->4->4->5
输出: 1->2->5

解析: 快慢指针

  1. 首先添加一个头节点,以防删除头节点
  2. 设置 slow ,fast 指针, slow 指向当前不重复的节点,fast 往前搜索重复节点。
class Solution:
    def deleteDuplication(self, head):
        if not head or not head.next:  # 若head为None,或者只有一个节点
            return head
        dummy = ListNode(-1)
        dummy.next = head  # 虚拟头节点,防止头节点被删除, 为了方便处理边界情况
        slow = dummy
        fast = dummy.next
        while fast:
            if fast.next and fast.next.val == fast.val: # 如果是重复的节点
                while fast.next and fast.val == fast.next.val:
                    fast = fast.next  # 指向最后一个重复的节点
                slow.next = fast.next  # 删除重复链表
                fast = fast.next  # 工作指针前移
            else: # 若不是重复的节点
                slow, fast = fast, fast.next  # 双指针前移
        return dummy.next

19 正则表达式匹配

题目: leetcode 10 牛客网

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。

‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

说明:

s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。

解析:

  1. 当p为空,s不为空时,肯定匹配失败,返回False。
    当p为空,s为空时, 匹配成功,返回True。
    当s为空,p不为空时还是有可能匹配成功。因为 ‘*’ 可以表示前面的字符出现0次
  2. 开始匹配字符,分两种情况:
    2.1. p下一个字符不为 ‘*’,就直接匹配当前字符。若字符相等或p为 ‘.’ 则成功,还要确保s没有遍历完。
    2.2. p下一个字符为 ‘*’,因为 ‘*’ 可以代表0个或者多个字符,考虑多种情况。
    a. 当 ‘*’ 匹配0个字符时,s不变,p往后移两位,即忽略 ‘a*’ 两个字符
    b. 当 ‘*’ 匹配了至少一个字符时,s往前移动一个字符,p不变。这里匹配一个或多个字符可以看作一个情况,因为:当匹配一个时,由于s移到了下一个字符,而p字符不变,就回到了情况 a。当匹配多于一个字符时,相当于从s的下一个字符继续开始匹配。
class Solution:
    def isMatch(self, s: str, p: str):
        """暴力递归,时间复杂度高
        """
        # 情况1
        if not p: return not s
        # s, p对应位置是否相等, bool(s)考虑s是否遍历完,遍历完还是有可能匹配成功,即忽略a*的情况
        first_match = bool(s) and p[0] in {s[0], '.'}
        if len(p) > 1 and p[1] == '*':  # 情况2.2
            return (self.isMatch(s, p[2:]) or  # 情况a
                    (first_match and self.isMatch(s[1:], p)))  # 情况b
        else:  # 情况2.1
            return first_match and self.isMatch(s[1:], p[1:])

    def isMatch_1(self, text, pattern):
        """记忆化,自顶向下,动态规划
        基本根据上面翻译过来,加了字典记录出现过的值,避免了重复计算
        i,j分别表示text, pattern当前的位置
        """
        def dp(i, j):
            # 如果存在,直接返回答案
            if (i, j) in memo: return memo[i, j]
            # 匹配完毕,直接返回答案
            if j == len(pattern): return i == len(text)
            # 匹配当前位置
            first = i < len(text) and pattern[j] in {text[i], '.'}
            # 若pattern的下一位是 '*',情况2
            if j + 1 < len(pattern) and pattern[j + 1] == '*':
                ans = dp(i, j + 2) or (first and dp(i + 1, j))  # 情况a 和 情况b
            else:
                ans = first and dp(i + 1, j + 1)  # 情况2.1
            # 记录答案
            memo[i, j] = ans
            return ans

        memo = {}
        return dp(0, 0)

    def isMatch_2(self, text, pattern):
        """自底向上,从后往前,动态规划
        dp[i][j] 表示 text[i:], pattern[j:]是否匹配
        """
        dp = [[False] * (len(pattern) + 1) for _ in range(len(text) + 1)]

        dp[-1][-1] = True  # 都为空,返回True
        for i in range(len(text), -1, -1):
            for j in range(len(pattern) - 1, -1, -1):
                first_match = i < len(text) and pattern[j] in {text[i], '.'}
                if j + 1 < len(pattern) and pattern[j + 1] == '*':
                    dp[i][j] = dp[i][j + 2] or (first_match and dp[i + 1][j])
                else:
                    dp[i][j] = first_match and dp[i + 1][j + 1]

        return dp[0][0]

20 表示数值的字符串

题目: leetcode 65

实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串’+100’, ‘5e2’, ‘-123’, ‘3.14’, ‘-1E-16’, 都表示
数值,但’12e’,‘1a3.14’, 都不是。

[-|+]A[.[B]][e|E[-|+]C].B[e|E[-|+]C]A是整数部分,B是小数部分,C是指数部分。AC前面可以有正负号。A不是必须的。

解析: 考虑各种情况

  1. 先去除两端空格,判断是否为空字符串
  2. 尽可能多的扫描有符号整数
  3. 判断小数点并扫描无符号整数
  4. 判断e,E并扫描有符号整数
  5. 随时保证不越界
class Solution:

    def isNumber(self, s: str) -> bool:
        s = s.strip()  # 去除两端空格
        if not s: return False
        self.i = 0  # 工作指针,指向字符串的当前位置
        numeric = self.scanInteger(s)
        # 如果出现‘.’,则接下来是小数部分
        if self.i < len(s) and s[self.i] == '.':
            self.i += 1  # 跳过'.'
            # 下面用or的原因
            # 1. 小数可以没有整数部分,如 .123 等于 0.123
            # 2. 小数点后面可以没有数字,如 233. 等于 233.0
            # 3. 当然,小数点前和后可以都有数字
            numeric = self.scanUnsignedInteger(s) or numeric  # 这里一定要先扫描小数点后的整数,再用or判断,不然可能会跳过0.8
        # 如果出现'e'或'E',则接下来是指数部分
        if self.i < len(s) and s[self.i] in {'e', 'E'}:
            self.i += 1  # 跳过'e, E'
            # 下面用and的原因
            # 1. e或E前面必须有数字,否则不成立
            # 2. e或E后面必须有整数
            numeric = numeric and self.scanInteger(s)
        return numeric and self.i == len(s)  # 若刚好遍历到字符串的下一个位置说明成功,否则说明被其他字符打断了

    def scanInteger(self, s):
        """扫描符号,并继续扫描整数
        """
        if self.i<len(s) and s[self.i] in {'+', '-'}:
            self.i += 1
        return self.scanUnsignedInteger(s)

    def scanUnsignedInteger(self, s):
        """扫描无符号整数
        """
        before = self.i
        while self.i < len(s) and '0' <= s[self.i] <= '9':  # 遇到异常字符则停止
            self.i += 1
        # 存在若干个数字时,则返回True
        return self.i > before

21 调整数组顺序使奇数位于偶数前

题目:牛客网

输入一个整数数组,实现一个函数来调整该数组中数字的顺序。

使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分。

解析:

解法一:不考虑相对位置

  1. 从前往后找偶数
  2. 从后往前找奇数
  3. 交换两者位置
  4. 随时保证不越界
def reOrderArray_1(self, arr):
    if not arr: return []
    l, r = 0, len(arr) - 1
    while l < r:
        while l < r and arr[l] & 1:  # 跳过奇数
            l += 1
        while l < r and not arr[r] & 1:  # 跳过偶数
            r -= 1
        if l < r:
            arr[l], arr[r] = arr[r], arr[l]  # 交换奇数偶数
    return arr

解法二:考虑之前的相对位置

用双向队列

从后往前扫描字符串,找到奇数放到队列前面

从前往后扫描字符串,找到偶数放到队列后面

def reOrderArray(self, arr):
    """不改变相对位置的算法, O(n)
    """
    if not arr: return
    from collections import deque
    q = deque()
    n = len(arr)
    for i in range(n):
        if not arr[i] & 1:  # 从前找偶数放到后面
            q.append(arr[i])
        if arr[n - 1 - i] & 1:  # 从后找奇数放到前面
            q.appendleft(arr[n - 1 - i])
    return q

22 链表中倒数第 K 个节点

题目: 牛客网

输入一个链表,输出该链表中倒数第k个结点。

解析: 快慢指针

快指针先走 k 步,快慢指针再一起走,快指针走到 NULL 时,慢指针走到倒数第 k 个节点

注:要判断 k < 链表长度的情况,返回空

class Solution:
    def FindKthToTail(self, head, k):
        if not head or not k: return
        fast = slow = head
        for _ in range(k):
            if not fast: return  # 若还没有走到k步就已经空了,则说明链表长度小于k
            fast = fast.next
        while fast:  # 直到走到尾节点的下一个节点
            fast, slow = fast.next, slow.next
        return slow

23 链表中环的入口节点

题目: leetcode 142

给定一个链表,若其中包含环,则输出环的入口节点。若其中不包含环,则输出null

解析:

剑指offer全书题解 (Python)【更新完毕】_第2张图片

  1. 设置快慢指针,假如有环,他们最后一定相遇。
  2. 两个指针分别从链表头和相遇点继续出发,最后一定相遇与环入口,tortoise为慢,hare为块。

剑指offer全书题解 (Python)【更新完毕】_第3张图片

class Solution(object):
    def detectCycle(self, head):
        if not head: return
        fast = slow = head
        # 检测是否有环
        while fast and fast.next:
            slow, fast = slow.next, fast.next.next
            if slow == fast:
                break
        else:  # 当fast为空,或者下一个节点为空时则说明没有环
            return
        # 找出入口节点
        while head != slow:  # 从头节点开始往前走 x 步,必定相遇
            head, slow = head.next, slow.next
        return head

24 反转链表

题目: leetcode 206

反转一个单链表。

解析:
解法一:循环

  1. 在遍历列表时,将当前节点的next指针改为指向前一个元素prev。
  2. 由于节点没有引用上一个节点,因此必须事先存储其前一个元素。
  3. 在更改引用之前,还需要另一个指针来存储下一个节点next。
  4. 最后返回新的头节点。
class Solution:
    def reverseList(self, head):
        prev = None  # 保存当前节点的上一个节点
        curr = head  # 当前节点
        while curr:
            next = curr.next  # 保存当前节点的下一个节点
            curr.next = prev  # 将当前节点指向上一个节点,即反转
            prev, curr = curr, next  # 指向上一个节点的指针和工作指针同时向前移动
        # 最后curr指向空时,prev刚好指向最后一个节点
        return prev

解法二:递归

递归版本关键在于反向工作。假设列表的其余部分已经被反转,现在我该如何反转它前面的部分?

假设列表为:

image.png

假设 n k + 1 n_{k+1} nk+1 n m n_m nm 已经反转完,而我们正处于 n k n_{k} nk

image.png

我们希望 n k + 1 n_{k+1} nk+1 的下一个节点指向 n k n_k nk

所以 n k n_k nk.next.next = n k n_k nk

要小心的是 n1 的下一个必须指向空,不然可能会产生循环。

def reverseList(self, head):
    if not head or not head.next:
        return head
    p = self.reverseList(head.next)  # 这返回的是尾节点
    head.next.next = head
    head.next = None # n1 的下一个必须指向空
    return p  # 返回尾节点

25 合并两个有序链表

题目: leetcode 21

将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

解析:

非递归:

  • 新建虚拟头节点
  • 依次加入两链表中小的那个节点
class Solution:
    def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
        dummy = cur = ListNode(-1)
        while l1 and l2:
            if l1.val < l2.val:
                cur.next = l1
                cur, l1 = l1, l1.next
            else:
                cur.next = l2
                cur, l2 = l2, l2.next
        cur.next = l1 or l2
        return dummy.next

递归:

递归的定义在两个链表里的 merge 操作

image.png

故,每次都是两个链表头部较小的一个剩下的元素的 merge 操作结果合并。

注意边界条件,确保 l1 和 l2 两链表都不为空,否则返回不为空的那个节点。

    def merge1(self, l1: ListNode, l2: ListNode) -> ListNode:
        """递归 优雅
        """
        if not l1 or not l2:
            return l1 or l2
        if l1.val < l2.val:
            # 因为l1小,所以l1与剩下的节点merge
            l1.next = self.merge1(l1.next, l2)
            return l1
        else:
            # 因为l2小,所以l2与剩下的节点merge
            l2.next = self.merge1(l1, l2.next)
            return l2

26 树的子结构

题目: leetcode 572

输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)

解析: 递归

  1. 搜索二叉树 A ,判断每个节点开始的子树是否包含树 B
  2. 若不包含,递归搜索树 A 的左右孩子
class Solution:
    def isSubtree(self, s: TreeNode, t: TreeNode) -> bool:
        """搜索
        """
        if not s or not t: return False  # 只要有一个为空则不匹配
        # 判断当前两节点是否为相同的树,否则沿着s的左右孩子继续搜索
        return self.isPart(s, t) or self.isSubtree(s.left, t) or self.isSubtree(s.right, t)

    def isPart(self, p1, p2) -> bool:
        """判断是否相等
        """
        # 完全匹配
        # 当两棵树同时为空,说明匹配成功
        if not p2 and not p1: return True
        # 当一个空一个不为空,或者两个值不相等说明匹配失败
        if (not p1 or not p2) or p1.val != p2.val: return False
        # 包含即可,不用完全匹配
        # 当p2为空,说明匹配完成
        # if not p2: return True  # 若p2搜索完了,则成功
        # 若p1先搜索完,或者两者的值不相等,失败
        # if not p1 or p1.val != p2.val: return False
        # 递归的匹配左右子树
        return self.isPart(p1.left, p2.left) and self.isPart(p1.right, p2.right)

27 二叉树的镜像

题目: 牛客网

输入一个二叉树,将它变换为它的镜像。

输入树:
      8
     / \
    6  10
   / \ / \
  5  7 9 11
输出树:
      8
     / \
    10  6
   / \ / \
  11 9 7  5

解析:

递归:自顶向下递归交换

非递归:使用栈实现dfs,依次交换

class Solution:
    def Mirror(self, root):
        """自顶向下递归交换
        """
        if not root: return
        root.left, root.right = root.right, root.left
        self.Mirror(root.left)
        self.Mirror(root.right)
        
    def fun(self, root):
        """非递归 利用栈实现dfs
        """
        stack = root and [root]  # 等价于 stack = None if not root else [root]
        while stack:
            n = stack.pop()
            if n:
                n.left, n.right = n.right, n.left  # 入栈前进行交换
                stack += n.right, n.left

28 对称的二叉树

题目: leetcode 101

给定一个二叉树,检查它是否是镜像对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

    1
   / \
  2   2
 / \ / \
3  4 4  3

解析:

如果同时满足下面的条件,两个树互为镜像:

  1. 它们的两个根结点具有相同的值。
  2. 每个树的右子树都与另一个树的左子树镜像对称。
class Solution:
    def isSymmetric(self, root):
        return self.dfs(root, root)

    def dfs(self, p1, p2):
        """递归遍历两棵树是否镜像
        """
        # 若两棵树同时遍历完,则成功
        if not p1 and not p2: return True
        # 若不是同时遍历完,则失败
        if not p1 or not p2: return False
        # 两者相等并且p1的左子树和p2的右子树相等,p1的右子树和p2的左子树相等返回True
        return p1.val == p2.val and self.dfs(p1.left, p2.right) and self.dfs(p1.right, p2.left)
    
    def isSymmetric(self, root: TreeNode) -> bool:
        """栈,非递归
        """
        if not root: return True
        s = [(root.left, root.right)]
        while s:
            p1, p2 = s.pop()        
            if not p1 and not p2: continue  # 可能提前遇到叶子节点,只能跳过,不能返
            if not p1 or not p2: return False
            if p1.val != p2.val: return False
            s += [(p1.left, p2.right), (p1.right, p2.left)]
        return True
    
    def isSymmetric(self, root: TreeNode):
        """队列
        """
        if not root: return True
        from collections import deque
        q = deque()
        q.append((root.left, root.right))
        while q:
            p1, p2 = q.popleft()
            if not p1 and not p2: continue
            if not p1 or not p2: return False
            if p1.val != p2.val: return False
            q += [(p1.left, p2.right), (p1.right, p2.left)]
        return True 

29 顺时针打印矩阵

题目: leetcode 54

给定一个包含 m x n 个元素的矩阵(m 行, n 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。

示例 1:

输入:
[
 [ 1, 2, 3 ],
 [ 4, 5, 6 ],
 [ 7, 8, 9 ]
]
输出: [1,2,3,6,9,8,7,4,5]

解析:

解法一:模拟

从00开始走,直到碰壁,或者访问过就换方向。走col*row步。

def spiralOrder1(self, matrix):
    """正常解法
    """
    res = []
    if not matrix: return res
    row, col = len(matrix), len(matrix[0])  # 长,宽
    visited = [[False] * col for _ in range(row)]  # 记录访问过的节点
    direct = [(1, 0), (0, 1), (-1, 0), (0, -1)]  # 右,下,左,上 方向的集合
    x, y, d = 0, 0, 0  # 起始点, 和方向的坐标  x代表横坐标,y纵坐标
    for _ in range(row * col):
        res.append(matrix[y][x]) # 注意第一个是纵坐标 y,第二个是横坐标 x
        visited[y][x] = True
        a, b = x + direct[d][0], y + direct[d][1]  # 在方向上移动,a,b为下一个点的坐标
        # 只要碰壁了,或者访问过了,就换方向
        if a < 0 or a >= col or b < 0 or b >= row or visited[b][a]:
            d = (d + 1) % 4
            a, b = x + direct[d][0], y + direct[d][1]  # 在新的方向上移动
        x, y = a, b
    return res

解法二:

先取第一行,删除,逆时针旋转矩阵,再取第一行,直到矩阵为空

def spiralOrder(self, m):
    res = []
    while m:
        res += m[0]  # 取第一行
        m = list(zip(*m[1:]))[::-1]  # 顺时针旋转
    return res

30 包含min函数的栈

题目: leetcode 155

设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。

  • push(x) – 将元素 x 推入栈中。
  • pop() – 删除栈顶的元素。
  • top() – 获取栈顶元素
  • getMin() – 检索栈中的最小元素。

解析:

  1. 数据栈。
  2. 辅助栈,与数据栈同push,pop。每次push插入当前元素后的最小值。
class MinStack:
    def __init__(self):
        self.s = []   # 数据栈
        self.m_s = [] # 最小栈,保存数据栈插入元素后的最小值。最后是和数据栈同步的push pop

    def push(self, x: int) -> None:
        self.s.append(x)
        if len(self.m_s) == 0 or x < self.m_s[-1]:
            self.m_s.append(x)
        else:
            self.m_s.append(self.m_s[-1])  # 这里必须要插入,不然后面pop就没了

    def pop(self) -> None:
        self.m_s.pop()
        return self.s.pop()


    def top(self) -> int:
        return self.s[-1]


    def getMin(self) -> int:
        return self.m_s[-1]

31 栈的压入、弹出序列

题目:leetcode 946

两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。

假设压入栈的所有数字均不相等。

例如:

压入序列{1, 2, 3, 4, 5},序列{4,5,3,2,1}对应该压栈序列的一个弹出序列。而{4,3,5,1,2}不可能是该压栈序列的弹出序列。

解析:

  1. 用一个辅助栈,模拟pushed序列,依次压栈。

  2. 若辅助栈顶元素 == popped序列首元素,两者同时出栈,并接着循环判断是否相等

  3. 最后popped序列为空则说明匹配成功。

class Solution:
    def validateStackSequences(self, pushed, popped):
        s = [] # 辅助栈
        for num in pushed:
            s.append(num)
            while s and s[-1] == popped[0]:
                popped.pop(0)
                s.pop()
        return not popped

32 从上往下打印二叉树

题目: 牛客网

从上往下打印出二叉树的每个节点,同层节点从左至右打印。

解析:

使用队列实现层序遍历

class Solution:
    def PrintFromTopToBottom(self, root):
        from collections import deque
        q = deque([root])
        res = []
        while q:
            node = q.popleft()
            if node:
                res.append(node.val)
                q += [node.left, node.right]
        return res

32.1 分行从上到下打印二叉树

题目: leetcode 102

给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。

解析:

层序遍历,记录每层节点 个数

def levelOrder1(self, root: TreeNode) -> List[List[int]]:
    if not root:
        return []
    from collections import deque
    res = []
    q = deque([root])
    while q:
        n = len(q)  # 上层的节点数目
        cur_level = []  # 存储上层节点的临时数组
        for _ in range(n):
            node = q.popleft()  # 出队n次,n为上一层的节点数
            cur_level.append(node.val)
            if node.left: q.append(node.left)  # 再依次入队
            if node.right: q.append(node.right)
        res.append(cur_level)  # 遍历完一层,加入结果集
    return res

32.2 之字形打印二叉树

题目: leetcode 103

给定一个二叉树,返回其节点值的 之字形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)

解析:

和上题一样,加一个方向变量

class Solution:
    def zigzagLevelOrder(self, root: TreeNode) -> List[List[int]]:
        if not root: return []
        from collections import deque
        q = deque([root])
        res = []
        i = -1  # 方向
        while q:
            n = len(q)  # 每层节点的数目
            cur = []
            for _ in range(n):
                node = q.popleft()
                cur.append(node.val)
                if node.left: q.append(node.left)  # 这样确保里面都不为空,方便
                if node.right: q.append(node.right)
            i = i * -1  # 换方向啦
            res.append(cur[::i])
        return res

33 二叉搜索树的后序遍历

题目: 牛客网

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。

解析:

后续遍历:[左,右,根],又因为是二叉搜索树,所以 左<根<右

序列最后一个元素是根,可以把序列分割为两部分,第一部分都小于它,第二部分都大于它。

若不满足则返回False,依次递归判断。

class Solution:
    def VerifySquenceOfBST(self, seq):
        def dfs(seq):
            if not seq: return True
            root = seq[-1] # 根
            i = 0
            while seq[i] < root: i += 1 # 先找到左边区域,这里最多遍历到最后一个元素,也就是根
            for x in seq[i:-1]: # 右边区域若存在,这里可能为空,不满足条件则返回False
                if x < root: return False
            # 递归判断左右
            return dfs(seq[:i]) and dfs(seq[i:-1])

        if not seq: return False
        return dfs(seq)

34 二叉树中和为某一值的路径

题目:

给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。

解析:

前序遍历二叉树,边遍历边加入沿途节点,若遍历到叶子节点且,路径和相等,则加入结果。否则弹出当前节点,回溯。

class Solution:

    def pathSum(self, root, sumv):
        def dfs(root, sumv):
            sumv -= root.val
            cur.append(root.val)
            if not root.left and not root.right and sumv == 0:
                res.append(cur[:])  # 这里一定要加入复制,否则后面会改变这里的值
            if root.left:
                dfs(root.left, sumv)
            if root.right:
                dfs(root.right, sumv)
            cur.pop()

        if not root: return []
        res = []
        cur = []
        dfs(root, sumv)
        return res

35 复杂链表的复制

题目: leetcode 138

给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。

要求返回这个链表的深拷贝

解析:

解法一:

  • 用一个哈希表存储所有节点和对应的复制,需要O(n) 的辅助空间

  • 遍历哈希表,链接新链表

class Solution:
    def copyRandomList(self, head: 'Node') -> 'Node':
        m = n = head
        cp = {None:None}  # 1. 不用判断head是否为None,2. 不用管节点的random和next是否为None, 省去很多判断
        # copy
        while m:
            cp[m] = Node(m.val, None, None)
            m = m.next
        while n:
            cp[n].next = cp[n.next]
            cp[n].random = cp[n.random]
            n = n.next
        return cp[head]

解法二:

原地修改,三步法,O(1) 的空间

  • 根据原始链表的每个节点 N 创建对应的 N‘ ,把 N’ 直接链接到 N 后面
  • 对每个 N‘ ,复制其原始节点的随机指针
  • 将长链表拆成两个链表。依次将每个节点指向下一个的下一个
def copyRandomList_1(self, head):
    """O(n) 无需额外空间"""
    if not head: return head
    p1 = p2 = p3 = head
    # 
    while p1:
        p1.next = Node(p1.val, p1.next, None)
        p1 = p1.next.next
    while p2:
        if p2.random:
            p2.next.random = p2.random.next
        p2 = p2.next.next
    new_head = p3.next
    while p3.next:  # p3.next存在,也就新链表的节点存在!
        p3.next, p3 = p3.next.next, p3.next  # 注意这里蛛节点依次变换指针,分别指向下一个的下一个节点
    return new_head

36 二叉搜索树与双向链表

题目: 牛客网

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。

要求不能创建任何新的结点,只能调整树中结点指针的指向。

注意

  • 需要返回双向链表最左侧的节点。

解析:

解法一:

  • 二叉搜索树,左<中<右,故中序遍历的序列为有序的
  • 根据有序序列建立双向链表
def Convert(self, root):
    def inOrder(root):
        if not root: return
        inOrder(root.left)
        res.append(root)
        inOrder(root.right)

    if not root: return
    res = []
    inOrder(root)
    for i in range(len(res)- 1):
        res[i].right = res[i + 1]
        res[i+1].left = res[i]
    return res[0]

解法二:

  • 中序遍历,记录一个pre的全局指针,指向上一个节点
  • 递归修改结点指针
def Convert_1(self, root):
    """中序遍历,递归
    """
    def dfs(root):
        if not root: return
        dfs(root.left)
        root.left = self.pre
        if self.pre:
            self.pre.right = root
        self.pre = root
        dfs(root.right)
    if not root: return
    self.pre = None  # 这里要用全局变量记录pre节点指针,因为pre一直在变。# 因为普通传参递归层数高的不会传给层数低,这个引用可以让递归层数高的改变的pre作用到层数低的函数中。
    dfs(root)
    while root.left:
        root = root.left
    return root

37 序列化二叉树

题目: leetcode 297

你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

解析:

  • 前序遍历编码字符串
  • 前序遍历解码为二叉树
class Codec:
    def serialize(self, root):
        if not root: return '$'
        return str(root.val) + ',' + self.serialize(root.left) + ',' + self.serialize(root.right)

    def deserialize(self, data):
        def dfs(nodes):
            val = next(nodes)
            # 不用判断nodes是否迭代完,因为最后肯定是$,会返回的
            if val == '$': return None 
            root = TreeNode(val)
            root.left = dfs(nodes)
            root.right = dfs(nodes)
            return root

        nodes = iter(data.split(','))
        root = dfs(nodes)
        return root

38 字符串的排列

题目: leetcode 46

给定一个没有重复数字的序列,返回其所有可能的全排列。

解析: 全排列

回溯法:

是一种通过探索所有可能的候选解来找出所有的解的算法。如果候选解被确认 不是 一个解的话(或者至少不是 最后一个 解),回溯算法会通过在上一步进行一些变化抛弃该解,即 回溯 并且再次尝

  • 数组可以分为第一个字符和后面所有字符两部分。记录一个指针 first 指向当前数组的第一个字符
  • 第一个字符与所有的字符进行交换(包括自己)。
  • 递归的对第二部分进行全排列
  • 回溯,还原数组,才能进行下一次交换。
  • 如果 first 超过了数组元素,说明排列完成,生成一个解
def permute(self, nums):
    def dfs(first):  # first 记录每次开始的第一个位置
        if first == n: # 当起始位置超越数组长度了,停止,加入结果集
            res.append(nums[:])  # 这里要加入其复制,不然传的是引用,会被后面修改
        for i in range(first, n):  # 第一个数字与后面所有数字依次交换
            nums[first], nums[i] = nums[i], nums[first]
            dfs(first + 1)  # 递归替换下一个位置
            nums[first], nums[i] = nums[i], nums[first]  # 回溯,还原,才能进行下一次交换!

    n = len(nums)
    res = []
    dfs(0)
    return res

38.2 字符串的所有组合

题目: leetcode 78

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

返回的结果包括空列表。

解析: 子集

解法一:动态规划,递推

dp[i] = dp[i-1] + [each+[nums[i]] for each in dp[i-1]]

  1. 将第一个字符丢到结果集中
  2. 将第二个字符丢到结果中,并将第二个字符与结果中所有元素的组合丢到结果中
  3. 依次类推直到遍历完字符串
def subsets_1(self, nums: List[int]) -> List[List[int]]:
    res = [[]]
    for x in nums:
    	res += [each + [x] for each in res] # res中有一个空列表,与其结合就是本身
    return res

解法二:二进制位 掩码

数组的每个元素,可以有两个状态:

1、不在子数组中(用 0 表示);
2、在子数组中(用 1 表示)。

从 0 到 2 的数组个数次幂(不包括)的整数的二进制表示就能表示所有状态的组合。

def subsets_3(self, nums: List[int]) -> List[List[int]]:
    """位运算
    共有n个字符,对应n位bit,总共有2**n种排列,哪一位为1则将对应位置的字符加入到子集中
    """
    if not nums: return []
    res = []
    for i in range(2**len(nums)): 
        sub = []
        for j in range(len(nums)):
            if i >> j & 1:
            	sub.append(nums[j])
    res.append(sub)

解法三:回溯,递归

依次加入,走到底了就回溯。

[1, 2, 3]
1, 12, 123, 13, 2, 23, 3

class Solution: 
    def subsets_3(self, nums):
        def dfs(first): # first 指向当前第一个元素
            res.append(sub[:])  # 复制并加入结果集
            if first == len(nums):  # 若first走完了数组,返回
                return
            # 这里与全排列相比,不用交换数字,即数组的顺序是固定的。只需要依次加入元素就可以。
            for i in range(first, len(nums)):  # 从当前位置到末尾依次遍历
                sub.append(nums[i])  # 加入当前元素
                dfs(i + 1)  # 从下一个位置开始递归
                sub.pop()  # 回溯,去除sub中的元素,为了下一次遍历

        res, sub = [], []
        dfs(0)
        return res

38.3 八皇后问题

题目: leetcode 51

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

即任意两个棋子不在同一行,同一列,同一条对角线。

解析:

解法一:回溯

判断该棋子是否在已有棋子的列上,主对角线,次对角线上

  • 对于所有的主对角线有 行号 + 列号 = 常数,且常数刚好为[0,1,2,3,…,2n-1]
  • 对于所有的次对角线有 行号 - 列号 = 常数. 且常数刚好为[0,1,2,3,…,-2,-1] 也是2n-1个

用一个一维数组queens保存棋子,索引表示行号,值表示列号。

回溯步骤:

  • 从第一行第一列开始遍历。循环列并且试图在每个 column 中放置皇后.
  • 若满足要求则放置棋子。
  • 停止条件:若棋子数目等于n,满足要求加入结果。
  • 回溯,拿掉棋子,恢复状态。

注意 :若把需要回溯的状态放入递归函数的变量可自动回溯,如下面第二个函数

我们如果把 queens, xy_dif, xy_sum 三个状态的变化全部放入递归函数的变量中,则不需要我们手动回溯了,递归时会自动还原上一次的变量。

class Solution_2:
    """回溯法
    """
    def solveNQueens(self, n: int):
        def backtrack():
            row = len(queens)  # 下标为行的索引
            if row == n:  # 若已有n个元素则满足要求
                output.append(queens[:])
            for col in range(n):  # 从第一行第一列开始遍历
                if col not in queens and xy_dif[row - col] and xy_sum[row + col]:  # 判断是否能放置棋子
                    queens.append(col)  # 放置棋子
                    xy_dif[row - col], xy_sum[row + col] = False, False
                    backtrack()  # 向下一行探索
                    queens.pop()  # 回溯,挪开棋子
                    xy_dif[row - col], xy_sum[row + col] = True, True

        queens = []
        xy_dif = [True] * (2 * n - 1)
        xy_sum = [True] * (2 * n - 1)
        output = []  # 所有的结果集
        backtrack()  # 开始回溯
        return [["." * i + "Q" + "." * (n - i - 1) for i in sol] for sol in output]

    def solveNQueens_2(self, n):
        """dfs,把需要回溯的状态放入递归函数的变量可自动回溯。
        """
        def dfs(queens, xy_dif, xy_sum):
            row = len(queens)  # 下标为行的索引
            if row == n:
                result.append(queens)
                return
            for col in range(n):  # q为列的索引
                if not(col in queens or row - col in xy_dif or row + col in xy_sum):
                    dfs(queens + [col], xy_dif + [row - col], xy_sum + [row + col])

        result = []
        dfs([], [], [])
        return [["." * i + "Q" + "." * (n - i - 1) for i in sol] for sol in result]

解法二:暴搜,慢

  1. 全排列出所有的组合。0~n-1
  2. 依次判断每个组合是否符合要求。
class Solution_1:
    """
    全排列出所有顶点组合,[0,1,2,3,4,5,6,7],索引代表行号,值代表列号,所以全排列的结果已满足行列关系。
    只需判断是否符合对角线的要求即可, 即两个皇后的横坐标和纵坐标的差值的绝对值是否相等。
    """
    def solveNQueens(self, alist):
        allAns = self.Permutation(alist)
        res = []
        for tempList in allAns:
            if self.Judge(tempList):
                res.append(tempList)
                print(tempList)
        return [["."*i + "Q" + "."*(n-i-1) for i in sol] for sol in res]

    def Permutation(self, pointArr):
        def perm(nums, p, q):  # p为一个位置的坐标,q为最后一个位置
            # 当所有字母用完的时候
            if p == q:
                res.append(nums[:])  # 这里要加入其复制,不然传的是引用,会被后面修改
                return  # 遍历完,结束返回
            for i in range(p, q):
                nums[p], nums[i] = nums[i], nums[p]  # 第一个数字与后面所有数字依次交换
                perm(nums, p + 1, q)  # 第一个数字后面的部分继续做全排列
                nums[p], nums[i] = nums[i], nums[p]  # 回溯,交换回来

        res = []
        perm(pointArr, 0, len(pointArr))
        return res

    def Judge(self, alist): # 判断一个数组是否符合要求
        length = len(alist)
        for i in range(length-1):
            for j in range(i+1, length):
                if abs(i - j) == abs(alist[i] - alist[j]):  # 绝对值表示正负对角线
                    return False
        return True

39 数组中出现次数超过一半的数字

题目: leetocode 168

给定一个大小为 n 的数组,找到其中的众数。众数是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。

解析:

解法一:摩尔投票法

如果我们把众数记为 +1 ,把其他数记为 −1 ,将它们全部加起来,显然和大于 0 ,从结果本身我们可以看出众数比其他数多。

我们维护一个计数器,如果遇到一个我们目前的侯选众数,就将计数器加一,否则减一。只要计算器等于0,我们就将 nums 中之前访问的数字全部忘记,并把下一个数字当作侯选的众数。

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        count, res = 0, None
        for x in nums:
            if count == 0:
                res = x
            count += 1 if x == res else -1
        return res

解法二:先排序,后取中位数

def majorityElement(self, nums: List[int]) -> int:
    """Time: O(nlogn), Space: O(n)
    将数组排序后,出现次数大于一半的数一定在数组的中间
    """
    nums.sort()
    return nums[len(nums) // 2]

40 最小的 k 个数

题目: leetcode 215 leetcode是求最大的第K个数,思想一样

在未排序的数组中找到第 k 个最小的元素。

解析:

解法一:堆 Time: O(nlogk), Space: O(k)

我们需要维护容量为k的容器,每次插入新元素,删除最大的元素。

所以我们可以维护一个大根堆,并保持堆的大小小于等于 k.

class Solution:
    def GetLeastNumbers(self, tinput, k):
        """大根堆,O(nlogk) O(k)
        创建一个大顶堆,将所有数组中的元素加入堆中,并保持堆的大小小于等于 k
        """
        if k > len(tinput) or k < 0: return []
        import heapq  # python 默认小根堆,所以构建大根堆时进堆和出堆的时候都要加负号
        heap = []
        # heapq.heapify(heap)
        for num in tinput:
            heapq.heappush(heap, -num)
            if len(heap) > k:
                heapq.heappop(heap)
        return sorted(-x for x in heap)

解法二:快速排序 O(n),O(logn)

因为快速排序每次都可以确定一个元素的最终位置 idx,idx 左边的所有元素都小于它。所以我们判断 idx 和 k 的位置,来选择往左或者往右继续搜索。

而在这里,由于知道要找的第 k 小的元素在哪部分中,我们不需要对两部分都做处理,这样就将平均时间复杂度下降到 O(N)。

class Solution:
    def GetLeastNumbers_Solution(self, nums, k):
        def partition(l, r):
            from random import randint
            i = randint(l ,r)
            nums[l], nums[i] = nums[i], nums[l]
            pivot = nums[l]
            while l < r:
                while l < r and nums[r] >= pivot:
                    r -= 1
                nums[l] = nums[r]
                while l < r and nums[l] <= pivot:
                    l += 1
                nums[r] = nums[l]
            nums[l] = pivot
            return l

        def find(l, r):
            idx = partition(l, r)
            if idx == k -1 : return nums[idx]
            elif idx < k -1 : return find(idx + 1, r)
            else: return find(l, idx - 1)
            
        if not nums or k <= 0 or k > len(nums): return []
        idx = find(0, len(nums) - 1)
        return sorted(nums[:idx])

解法三:K次冒泡 O(kn),O(1)

    def GetLeastNumbers_Solution(self, nums, k):
        if k > len(nums) or k <= 0: return []
        for i in range(k):
            for j in range(len(nums)-i-1):
                if nums[j] < nums[j+1]:
                    nums[j], nums[j+1] = nums[j+1], nums[j]
        return nums[-k:][::-1]

41 数据流中的中位数

题目: leetcode 295

数据流中的数是流动的,如何找到一个数据流中的中位数。

中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。

解析:

由于数据是从一个数据流中读出来的,所以数据的数目随着时间的变化而增加。如果用一个数据容器来保存从流中读出来的数据,则当有新的数据出来时就需要插入数据容器中。这个数据容器用什么定义最合适呢?

因此我们想找到一种相当快速的方法来插入容器,那么所产生的额外操作可能会减少。

并且我们想找到中位数,并不一定要排序,只需保证前一半的元素都小于这个数,后一半的元素都大于这个数

所以我们需要这样一种数据结构:

  • 快速插入
  • 快速访问最大最小值。

事实证明,有两种数据结构符合:

  1. 堆(或优先级队列)
  2. 自平衡二进制搜索树

堆是这道题的天然原料!向元素添加元素需要对数时间复杂度。它们还可以直接访问组中的最大/最小元素。

我们可以这样维护两个堆:

  1. 用于存储输入数字中较小一半的最大堆
  2. 用于存储输入数字中较大一半的最小堆

其次我们要保证两个堆是平衡的,一个堆的长度最多比另外一个多1。

插入

  • 方法一:

    • 把数字插入下面的最大堆。(这一操作会导致最大堆的长度总大于最小堆)
    • 若此时最大堆的堆顶元素大于最小堆的堆顶,则各自堆顶元素取出插入对方中。这样保证了下面的最大堆的所有元素都小于上面的最小的所有元素。
    • 若最大堆的长度比最小堆+1还大,就取出最大堆的堆顶放入最小堆中,保证两个堆的平衡!
  • 方法二:

    • 先将元素插入最小堆中,取堆顶元素再插入最大堆中。这就可以保证最大堆中的元素总比最小堆的小。
    • 保证两堆的平衡

取中位数:

  • 若两个堆长度相等,则返回两个堆顶元素和的一半
  • 若不相等,此时总长度为奇数,且最大堆最多比最小堆多一个元素,所有此时的中位数就是最大堆的堆顶元素!

注意python自带的只有最小堆。

时间复杂度:O(5*logn) + O(1) = O(logn)

  • 方法二最坏有五次push/pop操作,和一次取值操作

空间复杂度:O(n)

  • 用于在容器中保存输入的线性空间
import heapq as hq
class MedianFinder:

    def __init__(self):
        self.min_heap = []  # 存储上半部分数
        self.max_heap = []  # 存储下半部分数

    def addNum(self, num: int) -> None:
        hq.heappush(self.max_heap, -num)  # 因为新元素总是插入到max_heap, 所以max_heap的长度总会大于min_heap

        if self.min_heap and -self.max_heap[0] > self.min_heap[0]:
            maxv = -hq.heappop(self.max_heap)
            minv = hq.heappop(self.min_heap)
            hq.heappush(self.min_heap, maxv)
            hq.heappush(self.max_heap, -minv)

        if len(self.max_heap) > len(self.min_heap) + 1:  # 保证两个堆的平衡,maxheap长度不能超过minheap长度+1
            hq.heappush(self.min_heap, -hq.heappop(self.max_heap))

    def addNum_2(self, num):
        """
        1. 先将元素插入最小堆中,取堆顶元素再插入最大堆中。这就可以保证最大堆中的元素总比最小堆的小。
        3. 保证两个堆的平衡,maxheap长度不能超过minheap长度+1
        """
        hq.heappush(self.min_heap, num)
        hq.heappush(self.max_heap, -hq.heappop(self.min_heap)) 

        if len(self.max_heap) > len(self.min_heap) + 1:
            hq.heappush(self.min_heap, -hq.heappop(self.max_heap))

    def findMedian(self) -> float:
        if len(self.min_heap) == len(self.max_heap):  # 如果两个堆平衡
            return (-self.max_heap[0] + self.min_heap[0]) / 2.
        else:
            return -self.max_heap[0]

42 连续子数组的最大和

题目: leetcode 53

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

解析: 动态规划

用函数 f ( i ) f(i) f(i) 表示以第 i 个数字结尾的字数组的最大和,那么我们需要求出 m a x [ f ( i ) ] max[f(i)] max[f(i)]

递归公式:

KaTeX parse error: No such environment: equation at position 16: f(i) = \begin{̲e̲q̲u̲a̲t̲i̲o̲n̲}̲ \begin{cases} …

  • 当第 i - 1 个数字结尾的字数组中所有数字的和小于0时,如果把这个负数与第 i 个数相加,结果会比第 i 个数本身还要小。所以这种情况下,以第 i 个数字结尾的字数组就是本身。
  • 当第 i - 1 个数字结尾的字数组中所有数字的和大于0时,则与第 i 个数字累加就得到以第 i 个数字结尾的字数组中所有数字的和。

时间复杂度:O(n)

空间复杂度:O(n),因为存储了n个状态 f

优化空间复杂度,用一个变量更新 f ( i − 1 ) f(i - 1) f(i1) ,一个变量记录最大的连续子数组和。

class Solution:

    def maxSubArray(self, nums: List[int]) -> int:
        if not nums: return 0
        f = [nums[0]]
        for i in range(1, len(nums)):
            f.append(nums[i]+f[i-1] if f[i-1]>0 else nums[i])
        return max(f)
    
    def maxSubArray_2(self, nums: List[int]) -> int:
        # 优化空间复杂度,用滚动变量代替状态表示
        res = f = nums[0]
        for i in range(1, len(nums)):
            f = nums[i] if f < 0 else f + nums[i]
            res = max(res, f)
        return res

43 1 ~ n 整数中 1 出现的次数

题目: leetcode 233

给定一个整数 n,计算所有小于等于 n 的非负整数中数字 1 出现的个数。

示例:

输入: 13
输出: 6 
解释: 数字 1 出现在以下数字中: 1, 10, 11, 12, 13 。

解析: 归纳法 牛客网解析

设i为计算1所在数字的位数,i=1表示计算个位数的1的数目,10表示计算十位数的1的个数等。

一个数按位数进行分割,可以分为两部分,高位 n/(i*10),低位 n%(i*10)

当求数字第 i 位1的个数时,

  • 高位部分每个数包含 i 个 1,例如十位上的1,10~19,有十个
  • 低位部分判断是否在[i, i*2-1]范围内,最多i个1,最少0个1。例如十位上的1出现的范围在[10, 19],最多有10个,最少有 0 个。取决于 低位部分 - i + 1的值。
  • 可以直接判断 (低位部分-i+1) 和 i 取最小值,同时保证大于0

所以得到公式:高位 1 的个数 + 低位 1 的个数

i 位上 1 的个数:n // (i * 10) * i + min(max(n % (i * 10) - i + 1, 0), i)

class Solution:
    def countDigitOne(self, n):
        cnt, i = 0, 1
        while i <= n: # i 依次个十百位的算,直到大于 n 为止。
            cnt += n // (i * 10) * i + min(max(n % (i * 10) - i + 1, 0), i)
            i *= 10
        return cnt

44 数字序列中某一位的数字

题目:leetcode 400

在无限的整数序列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, …中找到第 n 个数字。

注意:此处序列是从1开始的。

解析: 解析法

分析得到:

  • 个位数有9个数,共9位
  • 百位数有90个数,共180位
  • 千位数有900个数,共2700位

所以解题步骤是:

  1. 确定是几位数(个十百位数),1代表个位数,2代表十位数,3代表百位数…
  2. 确定是几位数的第几个数(如百位数的第3个数,即 102)
  3. 确定是这个数的第几位(如102的第2位,即结果为0)

注意:上面的分析是基于数列从1开始的,若序列是从0开始的,可以事先将n+1,但是因为个位从0开始,导致有10个数,与后面不匹配。所以可以将0去掉,n又要-1,即抵消了。

class Solution:
    def findNthDigit_1(self, n: int) -> int:
        i = 1  # 位数,进制
        s = 9  # 表示i位数有多少个数
        # 确定是几位数(i),以及是几位数的第多少位(n)
        while n > i * s:
            n -= i * s  # 总位数减去第i位的位数
            s *= 10  # 下一位的位数
            i += 1  # 进制加1
        # 确定是哪个数, i位数的第一个数为 10**(i-1)
        number = 10**(i-1) + math.ceil(n/i) - 1
        # 确定是这个数的第几位
        idx = n%i if n%i else i
        return int(str(number)[idx - 1])

45 把数组排成最小的数

题目: 牛客网 Acwing

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

例如输入数组[3, 32, 321],则打印出这3个数字能排成的最小数字321323。

解析: 定义排序规则,Time:O(nlogn)

定义 ab < ba 时,a < b
用此规则进行排序,从小到大。拼接出来就是最小的数字。

注意python2和python3自定义排序规则的区别。python3要用cmp_to_key将cmp转化为key。

  1. 写一个cmp函数传入key
  2. 直接写个 lambda a, b: (a+b > b+a) - (a+b < b+a)

解释两者为什么等价

cmp(a, b):

  1. 当a>b, 返回 1
  2. 当a=b, 返回 0
  3. 当a

所以等价于如下:
cmp(a, b) <==> (a>b) - (a

(a>b) - (a

  1. 当a>b, 返回 1 - 0 = 1
  2. 当a=b, 返回 0 - 0 = 0
  3. 当a
from functools import cmp_to_key

class Solution:
    def PrintMinNumber(self, numbers):
        # python 2
        # if not numbers:
        #     return ""
        # nums = map(str, numbers)
        # nums.sort(lambda a, b: cmp(a+b, b+a))
        # return int(''.join(nums))

        # python3
        if not numbers:
            return ""
        nums = list(map(str, numbers))
        nums.sort(key=cmp_to_key(self.cmp))
        # nums.sort(key=cmp_to_key(lambda a, b: (a+b > b+a) - (a+b < b+a)))
        return int(''.join(nums))

    def cmp(self, a, b):
        s1 = a + b
        s2 = b + a
        if s1 > s2: return 1
        elif s1 < s2: return -1
        else: return 0

46 把数字翻译成字符串

题目: leetcode 91 Acwing(原书)

给定一个数字,我们按照如下规则把它翻译为字符串:

0翻译成”a”,1翻译成”b”,……,11翻译成”l”,……,25翻译成”z”。

一个数字可能有多个翻译。例如12258有5种不同的翻译,它们分别是”bccfi”、”bwfi”、”bczi”、”mcfi”和”mzi”。

注意:

原书上是从0开始表示。注意leetcode的上是从1开始表示,这会导致0无法单独存在表示为一个字符,前面必须要是1或者2。

解析: 动态规划

原书的题。从0开始表示。范围为‘0’ ~ ‘25’

  1. 状态表示

    f[i] 表示前 i 位数字有多少不同的表示方式,求 f[n]

  2. 状态转移方程

    if i 位和 i - 1 位拼起来可以翻译成一个字母时,此时有两种方案,一、i位翻译成一个字母,方案数为 f[i - 1],二、i 和 i - 1位拼接翻译为一个字母,此时的方案数为 f[i - 2],所以公式为

    f[ i ] = f[i - 1] + f[i - 2]

    else:

    f[ i ] = f[i - 1]

  3. 边界

    f[0] = 1 # 定义一个数字没有的时候方案数是1

    f[1] = 1 # 前一个数字只有1种翻译方式

class Solution:
    def numDecodings(self, s: str) -> int:
        n = len(s)
        f = [0] * (n + 1)  # f[i]表示前i位数字有多少种不同的表示方式,求f[n],故共有n+1个元素
        f[0], f[1] = 1, 1
        for i in range(2, n + 1): # 2~n 前n个数字
            # 此时要得到第 i-1 个和第 i 个数字,对应字符串下标 i-2 和 i-1
            if '10' <= s[i - 2:i] <= '25':
                f[i] = f[i - 1] + f[i - 2]
            else:
                f[i] = f[i - 1]
        return f[n]

若是leetcode上的题,从1开始表示字母,这会导致0无法单独存在表示为一个字符,前面必须要是1或者2。

范围为‘1’ ~ ‘26’

  • 如果当前位为0:
    • 如果前一位不是1或2,则这个0无法和任何数组成字母,代表整个串无法构成编码,直接返回0
    • 如果前一位是1或者2,则说明前一位和当前位能组成字母,这时候能构成的编码数目是和 i-2 位相同的,即:dp[i] = dp[i-2]
  • 如果当前位不为0:
    • 如果前一位和当前位能组成字母,则当前位的构成编码数目应该为前一位和前前位之和,即:dp[i] = dp[i-1] + dp[i-2]
    • 如果前一位和当前位不能组成字母,则当前位单独组成字母,即dp[i] = dp[i-1]

还要注意0不能是字符串的开头!

if not s or s[0] == '0':
    return 0
n = len(s)
f = [0] * (n + 1)
f[0], f[1] = 1, 1
for i in range(2, len(s)+1):
    if s[i-1] == '0':
        if s[i-2] in {'1', '2'}:
            f[i] = f[i - 2]
        else:
            return 0
    else:
        if '10' <= s[i - 2:i] <= '26':
            f[i] = f[i - 1] + f[i - 2]
        else:
            f[i] = f[i - 1]
return f[n]

47 礼物的最大价值

题目: Acwing 相似题 leetcode 62

在一个m×n的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于0)。

你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格直到到达棋盘的右下角。

给定一个棋盘及其上面的礼物,请计算你最多能拿到多少价值的礼物?

m,n>0

样例:

输入:
[[2,3,1],
[1,7,1],
[4,6,1]]

输出:19

解释:沿着路径 2→3→7→6→1 可以得到拿到最大价值礼物。

解析: 动态规划

  1. 状态表示:
    f[i, j] 为走到i, j这个格子的最大价值,所以我们要求的是 f[n, m]
  2. 状态转移方程:
    当前格子的最大价值等于转移过来的两个格子最大的那个的价值加上当前格子自身的礼物价值。
    而之前转移过来的两个格子只能在当前格子的上方和左方。
    f[i, j] = max(f[i-1,j], f[i,j-1]) + gift[i,j]
  3. 边界
    f[i, 0] = f[0, j] = 0,将边界全部置为0即可

注意:下标从1开始算,不用处理边界问题。f[i - 1]不会越界。

class Solution(object):
    def getMaxValue(self, grid):
        if not grid: return
        rows, cols = len(grid), len(grid[0])
        f = [[0] * (cols+1) for _ in range(rows+1)]  # 从1开始,所以多一个
        for i in range(1, rows+1): # 从1开始,不用判断边界
            for j in range(1, cols+1):
                f[i][j] = max(f[i - 1][j], f[i][j - 1]) + grid[i-1][j-1]
        return f[rows][cols]

此方法需要 O(rows*cols) 空间,可以优化到只需要 O(cols) 空间,即一行保存所有状态。
f[j] 之前的值为这一行的状态 f[i, j-1],f[j] 从自身到后面的值为上一行的状态 f[i-1][j],
就相当于状态是一行一行更新的,每次只需保存此行和上一行的状态。

def getMaxValue_2(self, grid):
    """优化空间O(n)"""
    rows, cols = len(grid), len(grid[0])
    f = [0] * (cols+1)
    for i in range(1, rows+1):
        for j in range(1, cols+1):
            f[j] = max(f[j], f[j - 1]) + grid[i-1][j-1]
    return f[cols]

48 最长不含重复字符的子字符串

题目: leetcode 3

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

假设字符串中只包含从’a’到’z’的字符。

解析:

双指针算法,滑动窗口
l, r两个指针看作滑动窗口两端,窗口的长度为 r - l + 1
当窗口内出现重复字符时,移除左边元素直到删除重复元素,继续移动窗口右端。

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        """滑窗法
        """
        if not s: return 0
        l = 0
        mem = set()
        max_len = 0
        for r in range(len(s)):
            while s[r] in mem:
                mem.remove(s[l])
                l += 1
            max_len = max(r - l + 1, max_len)
            mem.add(s[r])
        return max_len

优化的滑动窗口
上述的方法最多需要执行 2n 个步骤。事实上,它可以被进一步优化为仅需要 n 个步骤。
我们可以定义字符到索引的映射,而不是使用集合来判断一个字符是否存在。
当我们找到重复的字符时,我们可以立即跳过该窗口, 不需要逐渐增加 l。

def lengthOfLongestSubstring_2(self, s: str) -> int:
    maxlen, l = 0, 0
    pos = {}  # 记录每个出现字母的下标
    for r, ch in enumerate(s):
        if ch in pos:
            # 如果重复,则起始点为重复点的下一位开始
            l = max(pos[ch] + 1, l)  #这里max指l不能后退,有可能此时重复的数字比起始点l还要前
        pos[ch] = r
        maxlen = max(maxlen, r-l+1)
    return maxlen

49 丑数

题目: leetcode 264

编写一个程序,找出第 n 个丑数。丑数就是只包含质因数 2, 3, 5正整数。习惯上把 1 当作第一个丑数。

解析: 牛客网

一个丑数一定是由另一个丑数乘以2或者乘以3或者乘以5得到。即丑数为 p = 2 x ∗ 3 y ∗ 5 z p = 2 ^ x * 3 ^ y * 5 ^ z p=2x3y5z ,那么我们可以维护三个队列,乘以2的队列,乘以3的队列,乘以5的队列,每次三个队列头中的最小值即为下一个丑数。我们没有必要维护三个队列,只需要记录三个指针显示到达哪一步

class Solution:
    def nthUglyNumber(self, n: int) -> int:
        if not n:
            return
        # p2,p3,p5分别为三个队列的指针,
        p2, p3, p5 = 0, 0, 0
        res = [1]
        for _ in range(n-1):
            # min_num 为三个队列头选出来的最小数
            min_num = min(res[p2]*2, res[p3]*3, res[p5]*5)
            res.append(min_num)
            # 这三个if有可能进入一个或者多个,进入多个是三个队列头最小的数有重复的情况
            if res[p2]*2 == min_num:
                p2 += 1
            if res[p3]*3 == min_num:
                p3 += 1
            if res[p5]*5 == min_num:
                p5 += 1
        return res[-1]

50.1 第一个只出现一次的字符

题目:leetcode 387

给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。

解析:

  • 循环一遍字符串,用一个字典记录每个字符出现的次数。
  • 再次循环一遍字符串,一次判断每个字符出现的次数是否为 1
class Solution:
    def firstUniqChar(self, s: str) -> int:
        if not s:
            return -1
        count = {}
        for x in s:
            if x not in count:
                count[x] = 1
            else:
                count[x] += 1

        for key in count:
            if count[key] == 1:
                return s.index(key)
        return -1

50.2 字符流中第一个只出现一次的字符

题目: 牛客网

请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。

解析:

  • 利用一个int型数组表示256个字符,这个数组初值置为-1.

  • 每读出一个字符,将该字符的位置存入字符对应数组下标中。

  • 若值为-1标识第一次读入,不为-1且>0表示不是第一次读入,将值改为-2.

  • 之后在数组中找到>0的最小值,该数组下标对应的字符为所求

class Solution_2:
    
    def __init__(self):
        self.count = [-1] * 256  # -1 代表一次都没出现过,索引代表字符的ASCII码值
        self.index = 0  # 记录当前字符的个数,可以理解为输入的字符串中的下标

    def FirstAppearingOnce(self):
        min_value = self.index  # base,当前的最小值
        ch = '#'
        # 找出最小的索引
        for i in range(256):
            # 在索引大于0的地方寻找,比当前最小值的还小的话,更新当前的最小值索引,更新索引对应的字符
            if self.count[i] >= 0 and self.count[i] < min_value:
                min_value = self.count[i]
                ch = chr(i)
        return ch

    def Insert(self, char):
        # 如果是第一次出现,则将对应元素的值改为index
        if self.count[ord(char)] == -1:
            self.count[ord(char)] = self.index
        # 如果已经出现过一次,则修改为-2
        elif self.count[ord(char)] >= 0:
            self.count[ord(char)] = -2
        self.index += 1

51 数组中的逆序对

题目: 牛客网 Acwing

在数组中的两个数字如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。

输入一个数组,求出这个数组中的逆序对的总数。

解析: 分治法

其实就是归并排序。

假设分隔到最小的两个数组,p1指向 left 数组中的元素,p2指向 right数组中的元素,此时两数组内部是有序的。

  • 当left[p1] 大于 right[p2] 时,left中p1后面的所有元素(包括p1)都大于right[p2]。所以此时逆序对数 += len(left[p1:])
  • 当left[p1] 小于 right[p2] 时,正常合并即可。
class Solution:
    def inversePairs(self, nums):
        """分治法:O(nlogn) Space: O(n)"""
        self.count = 0
        self.merge_sort(nums)
        return self.count

    def merge_sort(self, nums):
        if len(nums) <= 1:
            return nums
        mid = len(nums) >> 1
        left = self.merge_sort(nums[:mid])
        right = self.merge_sort(nums[mid:])
        l, r = 0, 0  # 分别代表左右数组的指针
        tmp = []  # 存储排序数字的临时数组
        # 合并两数组到临时数组。
        while l < len(left) and r < len(right):
            if left[l] <= right[r]:
                tmp.append(left[l])
                l += 1
            else:
                tmp.append(right[r])
                r += 1
                # 逆序对数
                self.count += len(left[l:])
        tmp += left[l:] + right[r:]
        return tmp

52 两个链表的第一个公共节点

题目: leetcode 160

编写一个程序,找到两个单链表相交的起始节点。

剑指offer全书题解 (Python)【更新完毕】_第4张图片

如链表A和链表B。两链表在节点 c1 处相交。

解析:

解法一:哈希表法

用一个哈希表存储链表A的所有节点,然后检查链表 B 中的每一个结点 b_i 是否在哈希表中。若在,则 b_i 为相交结点。

  • 时间复杂度 : O(m+n)
  • 空间复杂度 : O(m)或O(n)
class Solution(object):
    def getIntersectionNode(self, headA, headB):
        dic = set()
        p1 = headA
        while p1:
            dic.add(p1)
            p1 = p1.next
        while headB:
            if headB in dic:
                return headB
            headB = headB.next

解法二:双指针法

消除长度差: 拼接链表A和链表B。p1,p2分别从两个链表的头部开始遍历,当走到尾节点时,则从另外一个链表的头部继续往前走。这样,若存在相交的点,p1,p2必会在一个点相遇。

剑指offer全书题解 (Python)【更新完毕】_第5张图片

class Solution(object):
    def getIntersectionNode(self, headA, headB):
        if not headA or not headB:
            return
        q, p = headA, headB
        while q != p:
            q = q.next if q else headB
            p = p.next if p else headA
        return q

53.1 数字在排序数组中出现的次数

题目: leetcode 34

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

解析: 二分法

因为有序查找,所以二分。O(logn)

第一次二分找到target的第一个位置,第二次二分找到target的最后一个位置。

class Solution(object):
    def searchRange(self, nums, target):
        if not nums: return -1, -1

        l, r = 0, len(nums) - 1  # 第一次二分查找,找到重复数字最左边的索引
        while l < r:
            mid = l + (r - l) // 2
            if nums[mid] < target: # 说明答案在右边,不包含mid
                l = mid + 1
            else:
                r = mid
        if nums[l] != target: return -1, -1  # 如果没有找到这个数,则说明不存在
        left = l  # 重复数字最左边的索引

        l, r = 0, len(nums) - 1  # 第二次二分查找,找到重复数字最右边的索引
        while l < r:
            mid = l + (r - l + 1) // 2
            if nums[mid] <= target: # 说明答案在右边,包含mid,故上面加1
                l = mid
            else:
                r = mid - 1
        return left, r

    def searchRange_2(self, nums, target):
        """暴力法, 一次循环
        """
        res = []
        for i, x in enumerate(nums):
            if x == target:
                res.append(i)
        if not res: return -1, -1
        return res[0], res[-1]

53.2 0~n-1 中缺失的数字

题目: leetcode 268

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0到n-1之内。

在范围0到n-1的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

解析:

因为有序,所以二分。

因为范围为0~n-1,所以缺失值的前半部分值都是等于下标的。从缺失值开始,值与下标就不会一一对应了。所以我们要找到第一个值与下标不相等的元素,那它的下标即我们要找的缺失值。

注意:当所有的数都与下标对应时,缺失值为下一个值。

``python

class Solution(object):
    def getMissingNumber(self, nums):
        if not nums: return 0
        l, r = 0, len(nums) - 1
        while l < r:
            mid = l + r >> 1
            if nums[mid] == mid:
                l = mid + 1
            else:
                r = mid
        if nums[l] == l: l += 1  # 当缺失的为最后一个数时, 返回 l++
        return l

考虑数组无序的情况

解法一:高斯公式求和

n项和减去数组和即为缺失的值。

    def getMissingNumber_2(self, nums):
        n = len(nums)
        sumv = sum(nums)
        return (0 + n)*(n + 1)//2 - sumv

解法二:异或法

由于异或运算(XOR)满足结合律,并且对一个数进行两次完全相同的异或运算会得到原来的数,因此我们可以通过异或运算找到缺失的数字。

我们知道数组中有 n 个数,并且缺失的数在 [0…n] 中。因此我们可以先得到 [0…n] 的异或值,再将结果对数组中的每一个数进行一次异或运算。未缺失的数在 [0…n]和数组中各出现一次,因此异或后得到 0。而缺失的数字只在 [0…n]中出现了一次,在数组中没有出现,因此最终的异或结果即为这个缺失的数字。

在编写代码时,由于 [0…n]恰好是这个数组的下标加上 n,因此可以用一次循环完成所有的异或运算

class Solution:
    def missingNumber(self, nums: List[int]) -> int:
        miss = len(nums)  # 假设缺失值为 n
        for i, num in enumerate(nums):
            miss ^= i ^ num
        return miss

53.3 数组中数值与下标相等的元素

题目:

假设一个单调递增的数组里的每个元素都是整数并且是唯一的。

请编程实现一个函数找出数组中任意一个数值等于其下标的元素。

例如,在数组[-3, -1, 1, 3, 5]中,数字3和它的下标相等

解析: 二分法

因为有序,所以二分。

假设第一个数值与下标相等的元素为m,则m右边的所有元素 小于 其索引,m左边的所有元素 大于 其索引

class Solution(object):
    def getNumberSameAsIndex(self, nums):
        l, r = 0, len(nums)
        while l < r:
            mid = l + r >> 1
            if nums[mid] < mid: l = mid + 1
            else: r = mid
        if nums[l] != l: return -1
        return nums[l]

54 二叉搜索树的第k小的节点

题目: 牛客网 Acwing

给定一棵二叉搜索树,请找出其中的第k小的结点。

你可以假设树和k都存在,并且1≤k≤树的总结点数。

解析:

中序遍历的同时,没遍历一次,k–,当k = 0时,为答案。

class Solution(object):
    def kthNode(self, root, k):
        def dfs(root):
            if not root: return
            dfs(root.left)
            self.k -= 1
            if self.k == 0: self.res = root
            if self.k > 0: dfs(root.right)  # 剪枝

        self.k = k  # k要为全局变量
        self.res = None
        dfs(root)
        return self.res

55.1 二叉树的深度

题目:

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

解析:

递归

class Solution:
    def maxDepth(self, root: TreeNode) -> int:
        if not root: return 0
        left = self.maxDepth(root.left)
        right = self.maxDepth(root.right)
        return max(left, right) + 1

55.2 平衡二叉树

题目: leetcdoe 110

给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。

解析:

递归的同时记录高度,可以考虑加上剪枝

    def isBalanced_2(self, root):
        def dfs(root):
            if not root: return 0
            left = dfs(root.left)
            right = dfs(root.right)
            if abs(left - right) > 1:
                self.ans = False
            return max(left, right) + 1
        self.ans = True
        dfs(root)
        return self.ans

56.1 数组中只出现一次的两个数字

题目: leetcode

给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。

解析:

解法一:哈希表

两遍循环,第一遍记录每个数字出现的次数。第二遍筛选出只出现一次的数字。

  • 时间复杂度O(2n)
  • 空间复杂度O(n)
class Solution:
    def singleNumber(self, nums: List[int]) -> List[int]:
        dic = {}
        for x in nums:
            if x not in dic:
                dic[x] = 1
            else:
                dic[x] += 1
        res = []
        for x in nums:
            if dic[x] == 1:
                res.append(x)
        return res

解法二:位运算。

  • 先进行一次异或运算,结果为这两个不同的数的异或结果。结果二进制中至少有一位1,记录为n位。

  • 根据第n位是否为1可将数组分为两个子数组。子数组中各存在一个 出现一次的数,且其他的数都出现两次。

  • 在子数组中进行异或运算,可得到一个出现一次的数字。再将这个数字与第一次的异或结果进行异或,即为第二个答案(异或的交换性)。

class Solution(object):
    def singleNumber(self, nums):
        mask = 0
        for num in nums:
            mask ^= num
        k = 0  # 找到最后为1的一位
        while not mask >> k & 1:
            k += 1
        first = 0
        for x in nums:
            if x >> k & 1:
                first ^= x
        return first, first ^ mask

56.2 数组中唯一只出现一次的数字

题目: leetcode 137

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。

解析:

如果一个数字出现三次,那么它的二进制表示的每一位(0或者1)也出现三次。如果把所有出现三次的数字的二进制表示的每一位都分别加起来,那么每一位都能被3整除。

把数组中所有数字的二进制表示的每一位都加起来。如果某一位的和能被 3 整除,那么那个只出现一次的数字二进制对应的那一为是 0,否则就是 1。

    def singleNumber(self, nums):
        ans = 0
        for i in range(32):
            bitCount = 0  # 第i位的 1 出现次数的总和
            for num in nums:
                if num >> i & 1:
                    bitCount += 1
            if bitCount % 3 != 0:  # 如果次数不能被 3 整除,则单独的那个数此位为1
                ans |= 1 << i
        return self.convert(ans)

    def convert(self, x):
        if x >= 2**31:
            x -= 2**32
        return x

57.1 和为 s 的数字

题目: Acwing

输入一个数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。

如果有多对数字的和等于s,输出任意一对即可。

你可以认为每组输入中都至少含有一组满足条件的输出。

解析:

因为递增有序,l, r指针分别指向最左和最右,即 l 指向最小的元素,r 指向最大的元素,若两者之和大于目标,r 往左移,若两者之和小于目标,l 往右移,等于则返回

class Solution:
    def FindNumbersWithSum(self, nums, target):
        l, r = 0, len(nums) - 1
        while l < r:
            if nums[l] + nums[r] == target:
                return nums[l], nums[r]
            elif nums[l] + nums[r] < target:
                l += 1
            else:
                r -= 1
        return []

57.2 和为 s 的连续正数序列

题目: 牛客网 Acwing

输入一个正数s,打印出所有和为s的连续正数序列(至少含有两个数)。

例如输入15,由于1+2+3+4+5=4+5+6=7+8=15,所以结果打印出3个连续序列1~5、4~6和7~8。

解析:

双指针,使用 l,r 分别指向序列的最左端和最右端,l 初始化为 1,r 初始化为 2,记录 l~r 的和为 curSum。

  • 如果curSum小于目标,r向前走,curSum加上r。
  • 如果curSum大于目标,curSum减去 l,l 向前走
  • 等与则把 l~r 的连续序列加入到结果中,r继续往前走,curSum加上r。
class Solution(object):
    def findContinuousSequence(self, sum):
        l, r, cur_sum = 1, 2, 3
        res = []
        mid = sum + 1 >> 1  # 至少两个数,所以小的那个数不可能大于和的一半。
        while l <= mid: 
            if cur_sum < sum:  # 当前和小于sum时
                r += 1  # 右移r
                cur_sum += r  # 加上新的数
            elif cur_sum > sum:  # 当前和大于sum时
                cur_sum -= l  # 减去最小数
                l += 1  # 右移l
            else:  # 等于时加入答案,并且继续右移指针
               res.append(list(range(l,r+1)))
               r += 1
               cur_sum += r
        return res

58.1 翻转字符串

题目: leetcode 151

给定一个字符串,逐个翻转字符串中的每个单词。

示例 1:

输入: "the sky is blue"
输出: "blue is sky the"

解析:

用python的话就不多说了。

class Solution:
    def reverseWords(self, s: str) -> str:
        return ' '.join(reversed(s.split()))

    def reverseWords_2(self, s: str):
        """两次翻转
        1. 翻转整个字符串
        2. 翻转每个单词
        """
        s = s.strip()
        s = list(s)
        s.reverse()
        res = ''
        for i in range(len(s)):
            j = i
            while j < len(s) and s[j] != ' ':
                j += 1
            res += reversed(s[i:j])
            i = j
        return ''.join(res)

58.2 左旋转字符串

题目: 牛客网 Acwing

请定义一个函数实现字符串左旋转操作的功能。

比如输入字符串"abcdefg"和数字2,该函数将返回左旋转2位得到的结果"cdefgab"

解析:

python 不用多说

class Solution(object):
    def leftRotateString(self, s, n):
        return s[n:] + s[:n]

59 滑动窗口的最大值

题目: 牛客网 Acwing

给定一个数组和滑动窗口的大小,请找出所有滑动窗口里的最大值。

例如,如果输入数组[2, 3, 4, 2, 6, 2, 5, 1]及滑动窗口的大小3,那么一共存在6个滑动窗口,它们的最大值分别为[4, 4, 6, 6, 6, 5]。

解析:

使用一个双端队列。队列中存入数组元素的下标

  • 准备插入时,判断插入后是否大于size,大则先弹出队首元素,再考虑插入。
  • 准备插入新元素时,先删除队列中比新元素小的所有元素,保留比它大的值(因为当前面大的元素弹出后,新元素有可能成为最大值)。
  • 插入新元素,若size >= k,开始记录最大值,即队首元素。
    def maxInWindows_2(nums, k):
        """两端开口的队列
        """
        from collections import deque
        if k <= 0: return []  # 异常输入
        res = []
        q = deque()
        for i in range(len(nums)):
            # 此时准备插入 i, 若i插入后元素大于k,则队首弹出元素,也就是给i留位置,等于事先判断
            if q and i - q[0] + 1 > k: # 当队列元素个数大于窗口size,从队首弹出元素
                q.popleft()
            # 从队尾开始比较,依次弹出比当前num小的元素,同时能保证队列首元素为当前窗口最大值索引,也能保证队首元素为窗口的第一个元素
            while q and nums[q[-1]] <= nums[i]:
                q.pop()
            q.append(i)  # 都处理完后,插入当前元素
            if i + 1 >= k: # 当形成完整的窗口时,开始写入最大值
                res.append(nums[q[0]])
        return res

60 n个骰子的点数

题目: Acwing

将n个骰子投掷1次,获得的总点数为s,s的可能范围为n~6n。

掷出某一点数,可能有多种掷法,例如投掷2次,掷出3点,共有[1,2],[2,1]两种掷法。

请求出投掷n次,掷出n~6n点分别有多少种掷法。

样例1

输入:n=1

输出:[1, 1, 1, 1, 1, 1]

解释:投掷1次,可能出现的点数为1-6,共计6种。每种点数都只有1种掷法。所以输出[1, 1, 1, 1, 1, 1]。

解析:

解法一:动态规划

状态表示:

  • dp[i][j] 表示 i 个骰子扔出和为 j 的可能数
  • dp[n][j] , j = [n, 6n]
  • 因为一个骰子的范围是 [1, 6],所以我们下标全部从 1 开始。

状态转移:

  • 因为上一个骰子扔出的点数可能为 [1, 6]
  • 所以 dp[i][j]=dp[i-1][j-1]+dp[i-1][j-2]+...+dp[i-1][j-6]

边界:

  • dp[0][0] = 1 ,当一个骰子都没有时,总和为0的情况下的方案数为 1
class Solution(object):
    def numberOfDice(self, n):
        # 每个地方索引+1都是因为方便计算。因为是从骰子个数和一个骰子的范围都是从 1 开始的。
        f = [[0]*(6*n + 1) for _ in range(n + 1)]
        f[0][0] = 1
        for i in range(1, n + 1): # 枚举骰子的个数
            for j in range(i, 6*i + 1): # 枚举骰子为 i 个时的总和 [i, 6*i]
                for k in range(1, min(j, 6) + 1):  # 上一个骰子的可能出现的点数
                    f[i][j] += f[i - 1][j - k]  # 确保k <= j
        return [f[n][i] for i in range(n, 6*n+1)]

解法二:递归

可能包含大量重复运算。

n 个 骰子的总和范围为 [n, 6n]。对于和的每一个取值,分别算出可能出现方案数。

    def numberOfDice_2(self, n):
        res = []
        for i in range(n, n*6+1):  # 总和的范围,依次枚举
            res.append(self.dfs(n, i))
        return res

    def dfs(self, n, sum): # n为骰子个数,sum为总和
        # 求解总和为某个n时的可能的投掷的所有方案数
        if sum < 0: return 0
        if n == 0: return not sum # n为0时,sum刚好等于0则是一种解决方案,大于0则说明方案不成立
        ans = 0 # 次数
        for i in range(1, 7): # 枚举所以的可能
            ans += self.dfs(n-1, sum - i)
        return ans

61 扑克牌中的顺子

题目: Acwing 牛客网

从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。

2~10为数字本身,A为1,J为11,Q为12,K为13,大小王可以看做任意数字。

为了方便,大小王均以0来表示,并且假设这副牌中大小王均有两张。

解析:

将 5 个数排序,找到第一个非零值的索引,首先判断数组后面是否有重复值,重复则不可能为顺子。

再判断非零的最大值与最小值的差是不是在四以内,在就可以用 0 补。

    def isContinuous_2(self, nums):
        """排序后
        非零的最大值与最小值的差是不是在四以内,在就可以用 0 补
        """
        if not nums: return False
        nums.sort()
        k = 0
        while not nums[k]: # 找到第一个不为0的元素的索引
            k += 1
        for i in range(k, len(nums)-1):
            if nums[i] == nums[i + 1]: # 若有重复的则不可能为顺子
                return False
        return nums[-1] - nums[k] <= 4

62 圆圈中最后剩下的数字

题目: Acwing

0, 1, …, n-1这n个数字(n>0)排成一个圆圈,从数字0开始每次从这个圆圈里删除第m个数字。

求出这个圆圈里剩下的最后一个数字。

解析:

总共n个人,从0开始编号,所以人0~n-1,每次从0开始报数,消灭第m个人,即编号m-1的人被消灭.
剩余人的编号从m开始,但是要从0开始重新开始编号并报数。

旧 m, m+1, m+2, ..., m-2
新 0, 1,   2,   ..., n-2  (此时总共n-1个人)
即 旧 = (新 + m) % n 注:这里的n是旧编号对应的n,新编号对应的是n-1了,因为消灭了一个人

我们记 f(n) 为 n个人中报数m最后活下来的人,报数一轮之后就剩n-1个人,活下来的人为f(n-1)
显然f(n) 和 f(n - 1) 为同一个人,只不过编号不一样了,为上面的对应关系。
到最后只剩一个人的时候,那个人就存活了,编号为0. 所以f(1) = 0,这是我们得到最新的编号,所以要往上递推回去。

        -- 0            , n==1
f(n) = | 
        -- (f(n-1)+m)% n, n>1
class Solution(object):
    def lastRemaining_2(self, n, m):
        """递推,最优解
        """
        if n <= 0 or m <= 0:
            return -1
        last = 0  # n=1的时候只剩一个人那么0就活了
        for i in range(2, n + 1):  # 从两个人开始
            last = (last + m) % i  # 每次模 上一次 的总人数
        return last

    def lastRemaining_1(self, n, m):
        """递归, 可能会爆栈
        """
        if n==1: return 0
        return (self.lastRemaining(n - 1, m) + m)%n

63 股票的最大利润

题目: leetcode 121

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。

解析:

动态规划。

剑指offer全书题解 (Python)【更新完毕】_第6张图片

当前位置的最大利润等于当前价格减去之前的最低价格。

状态表示:

  • f[i] 为卖出价为第 i 个数字时可能获得的最大利润
  • max(f[i])

转移方程

  • f[i] = nums[i] - minValueminValuei元素之前的最小价格
  • minValue = min(minValue, nums[i-1])

边界:

  • minValue = nums[0]
  • f[0] = 0 未买入前是不允许卖出的,所以第一个元素不能为卖出价,所以利润是0
class Solution:
    def maxProfit(self, nums: List[int]) -> int:
        if not nums: return 0
        f = 0 # f[0] = 0
        minPrice = nums[0]
        for i in range(1, len(nums)): # 枚举每个状态
            minPrice  = min(nums[i-1], minPrice)
            f = max(nums[i] - minPrice , f)
        return f

64 求 1+2+…+n

题目: 牛客网 AcWing

求1+2+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

解析:

递归

class Solution:
    def getSum(self, n):
        return n and (n+self.getSum(n - 1))

65 不用加减乘除做加法

题目: leetcode 371

不使用运算符 +- ,计算两整数 ab 之和。

解析:

将加法分为三部分:

  1. 求出无进制加法结果 s1

    我们先来观察下位运算中的两数加法,其实来来回回就只有下面这四种:

    0 + 0 = 0
    0 + 1 = 1
    1 + 0 = 1
    1 + 1 = 0(进位 1)
    

    等价与异或运算。

  2. 求出进位 s2

    在位运算中,我们可以使用与操作获得进位:

    a = 5 = 0101
    b = 4 = 0100
    a & b 如下:
    0 1 0 1
    0 1 0 0
    -------
    0 1 0 0
    

    由计算结果可见,0100 并不是我们想要的进位,1 + 1 所获得的进位应该要放置在它的更高位,即左侧位上,因此我们还要把 0100 左移一位,才是我们所要的进位结果。

  3. s = s1 + s2

因为python正数没有位数限制,可以用numpy来处理。

def getSum(num1, num2):
    import numpy as np
    while num2:
        sumv = np.int32(num1 ^ num2)
        carry = np.int32((num1 & num2) << 1)
        # 此时应该返回 sumv + carry,但是不能用加法
        # 所以循环,直到没有进位,也就不需要相加
        num1, num2 = sumv, carry
        return int(num1)

66 构建乘积数组

题目: AcWing

给定一个数组A[0, 1, …, n-1],请构建一个数组B[0, 1, …, n-1],其中B中的元素B[i]=A[0]×A[1]×… ×A[i-1]×A[i+1]×…×A[n-1]

解析:

class Solution:
    def multiply(self, A):
        n = len(A)
        if not n: return []
        # 先算第一部分:1, A0, A0A1, ... , A0A1An-2
        B = [1]
        for i in range(n-1):
            B.append(B[-1] * A[i])
        # 再算第二部分: 从后往前依次乘剩余的部分
        temp = 1  # 最后一项乘以1,B[n-1]=A0A1An-2
        for i in range(n-1, -1, -1):
            B[i] = B[i] * temp 
            temp *= A[i]
        return B

67 字符串转化为整数

题目: Acwing

请你写一个函数StrToInt,实现把字符串转换成整数这个功能。

当然,不能使用atoi或者其他类似的库函数。

样例

输入:"123"
输出:123

注意:

你的函数应满足下列条件:

  1. 忽略所有行首空格,找到第一个非空格字符,可以是 ‘+/−’ 表示是正数或者负数,紧随其后找到最长的一串连续数字,将其解析成一个整数;
  2. 整数后可能有任意非数字字符,请将其忽略;
  3. 如果整数长度为0,则返回0;
  4. 如果整数大于INT_MAX(2^31 − 1),请返回INT_MAX;如果整数小于INT_MIN(−2^31) ,请返回INT_MIN;

解析:

class Solution:
    def strToInt(self, s):
        self.input_valid = True
        if not s:
            self.input_valid = False  # 标志输如是否合法
            return 0
        k = 0
        while k<len(s) and s[k] == ' ':
            k += 1 # 去掉行首空格
        number = 0
        is_minus = False
        if s[k] == '+':
            k += 1
        elif s[k] == '-':
            k += 1
            is_minus = True
        while k < len(s) and '0' <= s[k] <= '9':
            number = number * 10 + int(s[k])
            k += 1
        if is_minus:
            number *= -1
        if number > 2 ** 31 - 1: # MAX_INT
            return 2 ** 31 - 1
        if number < - 2 ** 31:  # MIN_INT
            return -2**31
        return number

68 最低公共祖先

题目: leetcode 236

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

解析:leetcode

解法一:直接在递归的过程中判断。

解法二:搜出路径,再判断。

class Solution:
    def lowestCommonAncestor(self, root, p, q):
        self.ans = None
        self.dfs(root, p, q)
        return self.ans

    def dfs(self, root, p, q):
        if not root: return False
        left = self.dfs(root.left, p, q)
        right = self.dfs(root.right, p, q)
        # If the current node is one of p or q
        m = root == p or root == q
        # 左右子树和自身只要任意两个存在则返回答案
        if m + right + left >= 2:
            self.ans = root
        # 左中右只要有一个存在就返回True
        return m or right or left

    def lowestCommonAncestor_1(self, root, p, q):
        """先找到两条根节点到节点的路径
        再找到最后一个公共字子节点,则为最近公共祖先
        """
        if not root or not p or not q:
            return
        path_1 = []
        path_2 = []
        res = []
        self.dfs(root, p, path_1, res)
        self.dfs(root, q, path_2, res)
        path_1, path_2 = res[0], res[1]

        n = min(len(path_1), len(path_2))
        for i in range(n):
            if path_1[i] != path_2[i]:
                return path_1[i - 1]
            if i == n - 1:
                return path_1[i]

    def dfs(self, root, q, path, res):
        """递归获取根节点到某一节点的路径"""
        if not root: return
        path.append(root)
        if root == q:
            res.append(path[:])
            return  # 找到则停止递归
        self.dfs(root.left, q, path, res)
        self.dfs(root.right, q, path, res)
        path.pop()

你可能感兴趣的:(Python,数据结构与算法)