剑指offer刷题
- 15. 用两个栈实现队列
- 题目要求
- 解题思路
- 代码
- 16. 包含min函数的栈
- 题目要求
- 解题思路
- 代码
- 17. 从头到尾打印链表
- 题目要求
- 解题思路
- 代码
- 18. 反转链表
- 题目要求
- 解题思路
- 代码
- 19. 复杂链表的复制
- 题目要求
- 解题思路
- 代码
- 20. 替换空格
- 题目要求
- 解题思路
- 代码
- 21. 左旋转字符串
- 题目要求
- 解题思路
- 代码
- 22. 数组中重复的数字
- 题目要求
- 解题思路
- 代码
- 23. 最大子序和
- 题目要求
- 解题思路
- 代码
- 24. 第一个只出现一次的字符
- 题目要求
- 解题思路
- 代码
- 25. 二叉树——从上到下打印二叉树 I
- 题目要求
- 解题思路
- 代码
- 26. 二叉树——从上到下打印二叉树 II
- 题目要求
- 解题思路
- 代码
- 27. 二叉树——从上到下打印二叉树 III
- 题目要求
- 解题思路
- 代码
15. 用两个栈实现队列
题目要求
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
解题思路
- 辅助栈解法(类似汉诺塔);
- 把第二个栈做倒序。
代码
# class CQueue:
## 汉诺塔解法
# def __init__(self):
# self.stack1 = []
# self.stack2 = []
# def appendTail(self, value: int) -> None:
# # stack1 to stack2
# while self.stack1:
# self.stack2.append(self.stack1.pop())
# self.stack1.append(value)
# # stack2 to stack1
# while self.stack2:
# self.stack1.append(self.stack2.pop())
# def deleteHead(self) -> int:
# if self.stack1 == []: return -1
# return self.stack1.pop()
class CQueue:
# 倒序解法
def __init__(self):
self.stack1 = []
self.stack2 = []
def appendTail(self, value: int) -> None:
# add stack1
self.stack1.append(value)
def deleteHead(self) -> int:
# 法一
if self.stack2: return self.stack2.pop()
if self.stack1 == []: return -1
# 倒序
while self.stack1:
self.stack2.append(self.stack1.pop())
return self.stack2.pop()
def deleteHead(self) -> int:
# 法二
if not self.stack2:
if not self.stack1:
return -1
else:
while self.stack1:
self.stack2.append(self.stack1.pop())
return self.stack2.pop()
else:
return self.stack2.pop()
# Your CQueue object will be instantiated and called as such:
# obj = CQueue()
# obj.appendTail(value)
# param_2 = obj.deleteHead()
汉诺塔解法:
- 时间复杂度: appendTail()函数为 O(n) ;deleteHead() 函数也是O(n)。
- 空间复杂度: 均为 O(n) 。
倒序解法:
- 时间复杂度: appendTail()函数为 O(1) ;deleteHead() 函数在 N次队首元素删除操作中总共需完成
N个元素的倒序。 - 空间复杂度 : 最差情况下,栈 A 和 B 共保存 N个元素。
16. 包含min函数的栈
题目要求
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
解题思路
push和pop的时间复杂度均为O(1),但要实现min的时间复杂度也为O(1)就需要使用辅助栈B来保存数据栈A的最小值。这种方法的空间复杂度需要O(n),最坏的情况是保存所有值。
根据leetcode大佬们提供的思路,我选取了三种方法实现该算法。第一种是使用math.min来寻找最小值,第二种是使用if来判断,第三种是残差法(目前这个方法看不懂,后续看懂了再来补充)
代码
import math
class MinStack:
def __init__(self):
"""
initialize your data structure here.
"""
self.stack = []
self.min_stack = [math.inf]
def push(self, x: int) -> None:
self.stack.append(x)
# self.min_stack.append(min(x, self.min_stack[-1]))
if not self.min_stack or self.min_stack[-1] >= x:
self.min_stack.append(x)
def pop(self) -> None:
# self.stack.pop()
# self.min_stack.pop()
if self.stack.pop() == self.min_stack[-1]:
self.min_stack.pop()
def top(self) -> int:
return self.stack[-1]
def min(self) -> int:
return self.min_stack[-1]
# Your MinStack object will be instantiated and called as such:
# obj = MinStack()
# obj.push(x)
# obj.pop()
# param_3 = obj.top()
# param_4 = obj.min()
使用math.min来寻找最小值:
使用if来判断:
很明显,不调用math函数会提高运行速度。
17. 从头到尾打印链表
题目要求
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
解题思路
- 辅助栈:用一个列表来保存链表的数值,然后使用[::-1]倒序返回链表。
- 时间复杂度: 入栈和出栈一共使用 O(n) 时间。
- 空间复杂度: 辅助栈 result 和数组 res 共使用 O(n)的额外空间。
- 递归。时间复杂度: 遍历链表,递归 n 次。空间复杂度 : 系统递归需要使用 O(n) 的栈空间。
代码
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def reversePrint(self, head: ListNode) -> List[int]:
# 辅助栈 40ms
# result = []
# while head:
# result.append(head.val)
# head = head.next
# return result[::-1]
# 递归法 108ms
return self.reversePrint(head.next) + [head.val] if head else []
虽然两种方法的时间复杂度和空间复杂度是一样的。在具体的运行中,递归法的时间复杂度和空间复杂度会多很多。
18. 反转链表
题目要求
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
解题思路
- 迭代法。
- 时间复杂度: O(n)
- 空间复杂度: 由于没有开辟新的额外空间,因此空间复杂度为O(1)。
- 递归。
- 时间复杂度: O(n)
- 空间复杂度: O(n)
代码
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
# 迭代 24ms
# pre, cur = None, head
# while cur:
# tmp = cur.next
# cur.next = pre
# pre = cur
# cur = tmp
# return pre
# 递归 28ms
def recur(cur, pre):
if not cur: return pre
res = recur(cur.next, cur)
cur.next = pre
return res
return recur(head, None)
可以看出,递归方法虽然简单,但是会消耗大量的时间和占用空间。
19. 复杂链表的复制
题目要求
请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
解题思路
没咋看懂,代码是官网上大佬们的解答。第一种哈希表比较好懂。
https://leetcode-cn.com/problems/fu-za-lian-biao-de-fu-zhi-lcof/solution/jian-zhi-offer-35-fu-za-lian-biao-de-fu-zhi-ha-xi-/
代码
"""
# Definition for a Node.
class Node:
def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
self.val = int(x)
self.next = next
self.random = random
"""
class Solution:
def copyRandomList(self, head: 'Node') -> 'Node':
# 哈希表
# if not head: return None
# dic = {}
# cur = head
# while cur:
# dic[cur] = Node(cur.val)
# cur = cur.next
# cur = head
# while cur:
# dic[cur].next = dic.get(cur.next)
# dic[cur].random = dic.get(cur.random)
# cur = cur.next
# return dic[head]
# 直接复制
# return copy.deepcopy(head)
# 拼接+拆分
if not head: return None
cur = head
# 1. 复制各节点,并构建拼接链表
while cur:
temp = Node(cur.val)
temp.next = cur.next
cur.next = temp
cur = temp.next
# 2. 构建各新节点的 random 指向
cur = head
while cur:
if cur.random:
cur.next.random = cur.random.next
cur = cur.next.next
# 3. 拆分两链表
cur = res = head.next
pre = head
while cur.next:
pre.next = pre.next.next
cur.next = cur.next.next
pre = pre.next
cur = cur.next
pre.next = None # 单独处理原链表尾节点
return res
20. 替换空格
题目要求
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
解题思路
乍一看,这是一道相当简单的题目,但是需要实现空间复杂度和时间复杂度都最小,那还是需要动脑子滴。
熟悉Python的小伙伴都知道,替换直接用replace就好啦,所以这也是思路之一。
第二种就是使用列表,因为字符串是不可改变对象,我们把字符串元素都变成列表,然后再转换回字符串就好啦。
第三种是先把空格分割开,用“%20”替换空格。
代码
class Solution:
def replaceSpace(self, s: str) -> str:
# 用列表替换
# res = []
# for c in s:
# if c == " ":
# res.append("%20")
# else:
# res.append(c)
# return "".join(res)
# 直接用replace
# return s.replace(" ", "%20")
# 先把空格分割,再把“%20”添加上
s = s.split(" ")
return "%20".join(s)
下面的结果是用replace方法得到的,其时间复杂度和空间复杂度都是O(n)。
下面是用列表得到的结果,很明显速度快了很多。其中,这种方法的时间复杂度和空间复杂度也是O(n)。
第三种方法的时间复杂度和空间复杂度都与上面两种一致。
21. 左旋转字符串
题目要求
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
解题思路
- 字符串切片。 我一开始想到的这个方法,代码是这样的:
class Solution:
def reverseLeftWords(self, s: str, n: int) -> str:
if not s: return
res = s[:n]
s = s[n:] + res
return s
然后去参考大神们的解法,一句话就把这个解决了。return s[n:] + s[:n]
(大神不愧是大神!)
- 列表遍历拼接。 若面试不让使用切片函数,那就可以使用该方法。
https://leetcode-cn.com/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/solution/mian-shi-ti-58-ii-zuo-xuan-zhuan-zi-fu-chuan-qie-p/ - 其中,列表遍历拼接的第二种方法是取余。我整理了一下大神们使用该方法的理由:
遇到这类情况——如果n大于字符串长度,可以用取余来解决。就类似于数据结构的循环队列操作,n是线性增长的,通过取余可以让线性增长的数据在模内循环。 - 字符串遍历拼接。如果面试不让使用join(),可以采用该方法。
与方法二类似,只是用字符串替代列表即可。同样的,该方法也可以用取余,原因法二类似。
代码
- 字符串切片的时间复杂度和空间复杂度均为O(n)。
时间复杂度:n为字符串s的长度,字符串切片函数为线性时间复杂度;
空间复杂度:两个字符串切片的总长度为n。
class Solution:
def reverseLeftWords(self, s: str, n: int) -> str:
# 字符串切片
# 法一
# if not s: return
# res = s[:n]
# s = s[n:] + res
# return s
# 法二
return s[n:] + s[:n]
2.列表遍历拼接。时间复杂度和空间复杂度均为O(n)。
时间复杂度:线性遍历字符串s并进行添加操作,使用线性时间;
空间复杂度:创建新的列表res开辟了n大小的额外空间。
class Solution:
def reverseLeftWords(self, s: str, n: int) -> str:
# 列表遍历拼接
# res = []
# for i in range(n, len(s)): # O(n)
# res.append(s[i]) # append() is O(1)
# for i in range(n):
# res.append(s[i]) # O(1)
# return "".join(res) #O(n)
res = []
for i in range(n, n + len(s)):
res.append(s[i % len(s)])
return "".join(res)
- 字符串遍历拼接。
时间复杂度O(n):和列表遍历拼接类似,线性遍历字符串s并添加操作。
空间复杂度O(n):在字符串循环遍历过程中,内存会被回收,因此内存中至少同时存在长度n和n-1的两个字符串,所以至少使用O(n)的额外空间。
class Solution:
def reverseLeftWords(self, s: str, n: int) -> str:
# join不可用,可以使用字符串遍历拼接
# res = ""
# for i in range(n, len(s)):
# res += s[i]
# for i in range(n):
# res += s[i]
# return res
res = ""
for i in range(n, n + len(s)):
res += s[i % len(s)]
return res
以上三种方法的时间复杂度都是O(n),那为什么通过样例的时间和内存不一样呢?
K神的解释是:复杂度只是代表了随着输入数据的增长时的算法时间的增长,但是因为上面三种方法操作不一样,有的是用了for有的使用了取余,因此相同数据下的效率是有优劣的。
22. 数组中重复的数字
题目要求
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
限制:2 <= n <= 100000
解题思路
这道题有三种方法:
第一种,先排序然后遍历找到重复的数字,看起来思路简单,但是执行起来挺复杂的。时间复杂度是O(nlogn)。
第二种,哈希表。了解哈希表 这篇文章用很生动的对话把哈希表讲解了一遍。
- 时间复杂度:O(n)
- 空间复杂度:O(n)
第三种,原地交换。可以简单的理解为:①如果发现这个坑里的萝卜不是这个坑应该有的萝卜,就看看你是哪家的萝卜,然后把你送到你该去的地方,再把你家里现在的那个萝卜拿回家。②拿回家之后,再看看拿回来的萝卜是不是属于哪家的,再去找对应的家进行交换。③把上面的过程重复,直到把这个萝卜送回家时,发现这个家里面已经有萝卜了,出现重复了,这个萝卜就是多出来的。
- 时间复杂度:O(n)
- 空间复杂度:O(1)
代码
class Solution:
def findRepeatNumber(self, nums: List[int]) -> int:
# 哈希表
dic = set()
for num in nums:
if num in dic: return num
dic.add(num)
return -1
# 原地交换
for i in range(len(nums)):
while nums[i] != i: #如果遇到下标i与nums[i]不一样,那么就要把这个nums[i]换到它应该去的下标下面
temp = num[i] # 用temp变量保存这个nums[i]
if nums[temp] == temp: # 如果那么下标下面已经被占了,那么就找到了重复,结束就好了!
return temp
else:
nums[temp], nums[i] = nums[i], nums[temp]
哈希表
原地交换
疑问:看到上面两种解法明明是原地交换的空间复杂度比较低,那为什么内存消耗是一样的呢?
笔者在X乎上找到了答案。https://www.zhihu.com/question/380427506/answer/1089006544
23. 最大子序和
题目要求
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
提示:
1 <= nums.length <= 3 * 104
-105 <= nums[i] <= 105
进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。
解题思路
这道题可以有三种解法,暴力法、动态规划和题目所提示的分治法。
- 暴力法思路可以参考链接,其时间复杂度是O(n2)。
- 动态规划思路就是找到当前指针的最大子序和,可能保留之前的数,也可以抛弃之前的数,取决于它是否“拖累”当前指针的最大子序和。
时间复杂度:O(n),仅遍历一遍数组。
空间复杂度:O(1),没有使用其他额外空间。 - 分治法。参考链接1、 参考链接2分治法使用的是递归的形式,把一个数组分成左右子数组,把它分到已经不可再分为止。然后通过比较左右两边数组的最大子序和。不仅如此,还需要对比把两边子数组合起来的最大数值,即查看中间线是否把最大子序分开,进行比较。
最后把左边、右边和中间三个最大子序和进行比较,得到最后的结果。
时间复杂度:O(nlogn)。
代码
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
# 动态规划
for i in range(1, len(nums)): #range范围是[1,len(nums)) 左开右闭
# 若当前指针指向元素之前的和小于0,则丢弃此元素之前的数列(拖后腿的丢弃!!!)
# 当前和 = “当前值” 与 “当前值+之前最大和” 的比较中较大的那个
# 当前索引i永远存储0~i的最大和
nums[i] = max(nums[i]+ nums[i - 1], nums[i])
# 返回每个索引最大和的最大值
return max(nums)
# 分治
return(self.divid_and_conquer(nums, 0, len(nums)-1))
def divid_and_conquer(self, nums, low, high): # 分治函数,要输入数组和左右边界
if low == high: # 分治使用了递归的方法,当左指针和右指针指向同一个数,意味着已经不可再分了,则返回当前数
return nums[low]
mid = (low + high)//2 # 把一个数组从中间分成左右两个子数组
max_sum_left = self.divid_and_conquer(nums,low,mid) # 再对左子数组分成左右子子数组(递归计算左子数组中最大子序和)
max_sum_right = self.divid_and_conquer(nums,mid+1,high) # 同理,对右子数组分成左右子子数组(递归计算右子数组中最大子序和)
# 计算中间最大子序和
# 从右到左计算左边的最大子序和
cross_suffix = 0
sum = 0
for i in range(mid-1,low-1,-1):
sum += nums[i]
cross_suffix = max(sum, cross_suffix)
# 从左到右计算右边的最大子序和
cross_prefix = 0
sum = 0
for i in range(mid+1,high+1):
sum += nums[i]
cross_prefix = max(sum, cross_prefix)
cross_sum_max = cross_prefix + cross_suffix + nums[mid]
# 返回左边、中间和右边的最大子序和
return max(max_sum_left,cross_sum_max,max_sum_right)
24. 第一个只出现一次的字符
题目要求
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
解题思路
看到统计数字或者返回是否出现过的值,就知道要使用哈希表。
下面写了两种思路哈希表的思路,一种是无序的,一种是有序的。
哈希表:
- 先遍历一遍字符串s,然后把所有的字符串进行统计;
not char in dic
这个就很巧妙,not c in dic
整体为一个布尔值,c in dic
为判断字典中是否含有键c
。 - 然后再遍历一遍s,在哈希表中找到首个 “数量为 1的字符”,并返回。
时间复杂度:O(n),n是字符串的长度,需要遍历两次为O(2n),因此依然是O(n)。
空间复杂度:O(1), s 只包含小写字母,因此空间复杂度是O(26)=O(1)的额外空间。
有序哈希表:
- 同样的,需要遍历一遍字符串s把所有的字符进行统计;
- 然后再将dic遍历一遍。然后根据
dic.items()
python字典的特性遍历,获取结果。
时间复杂度和空间复杂度同哈希表方法是一样的。 参考链接
代码
class Solution:
def firstUniqChar(self, s: str) -> str:
# 哈希表
if not s: return " "
dic = {}
for char in s:
dic[char] = not char in dic
for char in s:
if dic[char]: return char
return " "
# 有序哈希表
# dic = {}
# for char in s:
# dic[char] = not char in dic
# for k, v in dic.items():
# if v: return k
# return " "
哈希表:
有序哈希表:
25. 二叉树——从上到下打印二叉树 I
题目要求
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
解题思路
从上到下遍历,称为二叉树的广度优先搜索(BFS),BFS通常借助“队列”的先入先出实现。
时间复杂度O(N):N是二叉树的节点数量,BFS需要循环N次。
空间复杂度O(N):最差情况下,即当树为平衡二叉树时,最多有 N/2个树节点同时在 queue 中,使用 O(N)大小的额外空间。
代码
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def levelOrder(self, root: TreeNode) -> List[int]:
if not root: return []
# 从上至下遍历,可以使用二叉树的广度优先搜索(BFS),通常借助队列的先入先出来实现
# res, queue = [], [root]
res, queue = [], collections.deque()
queue.append(root)
while queue:
# cur = queue.pop(0) # 删除第一个元素
cur = queue.popleft()
res.append(cur.val)
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
return res
26. 二叉树——从上到下打印二叉树 II
题目要求
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
解题思路
从上到下遍历,称为二叉树的广度优先搜索(BFS),BFS通常借助“队列”的先入先出实现。
时间复杂度O(N):N是二叉树的节点数量,BFS需要循环N次。
空间复杂度O(N):最差情况下,即当树为平衡二叉树时,最多有 N/2个树节点同时在 queue 中,使用 O(N)大小的额外空间。
代码
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if not root: return []
# res, queue = [], [root]
res, queue = [], collections.deque([root])
while queue:
temp = []
for _ in range(len(queue)):
# cur = queue.pop(0)
cur = queue.popleft()
temp.append(cur.val)
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
res.append(temp)
return res
27. 二叉树——从上到下打印二叉树 III
题目要求
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
解题思路
从上到下遍历,称为二叉树的广度优先搜索(BFS),BFS通常借助“队列”的先入先出实现。
时间复杂度O(N):N是二叉树的节点数量,BFS需要循环N次。
空间复杂度O(N):最差情况下,即当树为平衡二叉树时,最多有 N/2个树节点同时在 queue 中,使用 O(N)大小的额外空间。
代码
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if not root: return []
res, queue = [], collections.deque([root])
while queue:
temp = collections.deque()
for _ in range(len(queue)):
cur = queue.popleft()
if len(res) % 2:
temp.appendleft(cur.val)
else:
temp.append(cur.val)
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
res.append(list(temp))
return res