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

文章目录

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} &amp; a_{n} \\ a_{n} &amp; a_{n-1} \\ \end{bmatrix}= \begin{bmatrix} 1 &amp; 1 \\ 1 &amp; 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 &amp; 1 \\ 1 &amp; 0 \\ \end{bmatrix},则A^1 = \begin{bmatrix} a_{2} &amp; a_{1} \\ a_{1} &amp; 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} &amp; a_{n-1} \\ a_{n-1} &amp; a_{n-2} \\ \end{bmatrix} \times \begin{bmatrix} a_{2} &amp; a_{1} \\ a_{1} &amp; a_{0} \\ \end{bmatrix}= \begin{bmatrix} a_{n+1} &amp; a_{n} \\ a_{n} &amp; 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} &amp; \text {n 为偶数} \\ a^{(n-1)/2} \cdot a^{(n-1)/2} \cdot a &amp; \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] )。

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

解析:

二分法

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

image.png

由图可以看到,线段由两段递增序列组成,左边大于等于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 >> 
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值