初级算法设计问题
这类问题通常要求你实现一个给定的类的接口,并可能涉及使用一种或多种数据结构。 这些问题对于提高数据结构是很好的练习。
一. 打乱数组
给你一个整数数组 nums ,设计算法来打乱一个没有重复元素的数组。
实现 Solution class:
Solution(int[] nums)
使用整数数组 nums 初始化对象
int[] reset()
重设数组到它的初始状态并返回
int[] shuffle()
返回数组随机打乱后的结果
示例:
输入
[“Solution”, “shuffle”, “reset”, “shuffle”]
[[[1, 2, 3]], [], [],[]]
输出
[null, [3, 1, 2], [1, 2, 3], [1, 3, 2]]
解释
Solution solution = new Solution([1, 2, 3]);
solution.shuffle(); //打乱数组 [1,2,3] 并返回结果。任何 [1,2,3]的排列返回的概率应该相同。例如,返回 [3, 1, 2]
solution.reset(); // 重设数组到它的初始状态 [1, 2, 3] 。返回 [1, 2, 3]
solution.shuffle(); // 随机返回数组 [1, 2, 3] 打乱后的结果。例如,返回 [1, 3, 2]
提示:
1 <= nums.length <= 200
-106 <= nums[i] <= 106
nums 中的所有元素都是 唯一的
最多可以调用 5 * 104 次 reset 和 shuffle
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-easy/xn6gq1/
来源:力扣(LeetCode)
解法
1.暴力打乱
用一个temp临时数组复制原数组的值,从中随机选取一个,按顺序放在原数组中。
暴力算法简单的来说就是把每个数放在一个 ”帽子“ 里面,每次从 ”帽子“ 里面随机摸一个数出来,直到 “帽子” 为空。下面是具体操作,首先我们把数组 array 复制一份给数组 aux,之后每次随机从 aux 中取一个数,为了防止数被重复取出,每次取完就把这个数从 aux 中移除。重置 的实现方式很简单,只需把 array 恢复称最开始的状态就可以了。
这种方法能保证等概率的正当性:
class Solution:
def __init__(self, nums: List[int]):
# 深层拷贝,另外储存一份
self.prev = list(nums)
# 指向同一地址
self.shuf = nums
def reset(self) -> List[int]:
"""
Resets the array to its original configuration and return it.
"""
self.shuf = list(self.prev)
return self.shuf
def shuffle(self) -> List[int]:
"""
Returns a random shuffling of the array.
"""
temp = list(self.shuf)
n = len(self.shuf)
for i in range(n):
random_index = random.randrange(len(temp))
self.shuf[i] = temp.pop(random_index)
return self.shuf
这里用到了random.randrange()
函数,参数同range函数一样,从给点的[start, end)返回一个随机数。
从 range(start, stop, step) 返回一个随机选择的元素。 这相当于 choice(range(start, stop, step)) ,但实际上并没有构建一个 range 对象。
这个随机数作为下标,从temp中随机选取并删除。
注意范围要选len(temp)
,因为temp长度是不断减少的。
在最初的__init__
函数,reset
函数,shuffle
函数中,使用
self.prev = list(nums)
self.shuf = list(self.prev)
temp = list(self.shuf)
外边加list()表示对原数组nums进行深层拷贝,等于在新地址中生成了一个数组,对原本数组的操作不会影响到它。
列表默认赋值浅拷贝特性 aux = list(self.array) = copy.deepcopy(self.array)等于一个新的内存地址深拷贝 id(aux) != id(array)
复杂度分析:
- 时间复杂度:
O
(
n
2
)
O(n^2)
O(n2),乘方时间复杂度来自于
list.pop
。每次操作都是线性时间(序号是随机的,pop(i)
的复杂度是 O ( n ) O(n) O(n))的,总共发生 n n n 次。 - 空间复杂度: O ( n ) O(n) O(n),为了复原数组,需要额外空间将数组另外储存一份。
2. Fisher-Yates 洗牌算法
不用新生成数组,而是将下标与当前下标到数组末尾元素之间的对应元素进行互换。
Fisher-Yates 洗牌算法跟暴力算法很像。在每次迭代中,生成一个范围在当前下标到数组末尾元素下标之间的随机整数。接下来,将当前元素和随机选出的下标所指的元素互相交换 - 这一步模拟了每次从 “帽子” 里面摸一个元素的过程,其中选取下标范围的依据在于每个被摸出的元素都不可能再被摸出来了。此外还有一个需要注意的细节,当前元素是可以和它本身互相交换的 - 否则生成最后的排列组合的概率就不对了。
只有打乱部分与第一个方法不同。
def shuffle(self) -> List[int]:
"""
Returns a random shuffling of the array.
"""
n = len(self.shuf)
for i in range(n):
random_index = random.randrange(i, n)
self.shuf[i], self.shuf[random_index] = self.shuf[random_index], self.shuf[i]
return self.shuf
复杂度分析:
- 时间复杂度: O ( n ) O(n) O(n),洗牌算法时间复杂度是线性的,交换元素操作,是常数复杂度。
- 空间复杂度: O ( n ) O(n) O(n),为了复原数组,需要额外空间将数组另外储存一份。
时间复杂度得到优化。
二. 最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
push(x)
—— 将元素 x 推入栈中。pop()
—— 删除栈顶的元素。top()
—— 获取栈顶元素。getMin()
—— 检索栈中的最小元素。
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-easy/xnkq37/
来源:力扣(LeetCode)
解法
1. 辅助栈
除了储存元素的栈外,另外定义一个栈储存每个元素入栈时对应的最小值。
对于栈来说,如果一个元素 a 在入栈时,栈里有其它的元素 b, c, d,那么无论这个栈在之后经历了什么操作,只要 a 在栈中,b, c, d 就一定在栈中,因为在 a 被弹出之前,b, c, d 不会被弹出。
因此,在操作过程中的任意一个时刻,只要栈顶的元素是 a,那么我们就可以确定栈里面现在的元素一定是 a, b, c, d。
那么,我们可以在每个元素 a 入栈时把当前栈的最小值 m 存储起来。在这之后无论何时,如果栈顶元素是 a,我们就可以直接返回存储的最小值
m。
两个栈之间的对应关系如下图。
class MinStack:
def __init__(self):
"""
initialize your data structure here.
"""
# self.minvalue = 2**32 + 1
self.stack = []
self.minstack = [ 2**32 + 1]
def push(self, val: int) -> None:
self.stack.append(val)
# 要将当前的最小值与val比较决定最小值,而不是历史最小值
self.minstack.append(min(self.minstack[-1], val))
def pop(self) -> None:
self.stack.pop()
self.minstack.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.minstack[-1]
复杂度分析:
- 时间复杂度: O ( 1 ) O(1) O(1),最小栈中所有操作的时间复杂度都是 O ( 1 ) O(1) O(1),每个操作最多调用栈操作两次。
- 空间复杂度: O ( n ) O(n) O(n),其中 n n n为总操作数。最坏情况下,我们会连续插入 n n n个元素,此时两个栈占用的空间为 O ( n ) O(n) O(n)。
2. 栈存储差值
栈不再存储push进来的元素值,而是存储push进来的元素值与先前最小值之间的差值,如果差值小于0说明push进来元素值小于最小值,就需要更新最小值。
这种方法不需要辅助栈,不需要额外空间。
class MinStack:
def __init__(self):
"""
initialize your data structure here.
"""
self.stack = []
self.minvalue = 0
def push(self, val: int) -> None:
# 空栈时的操作
if not self.stack:
self.stack.append(0)
self.minvalue = val
else:
# 存储差值,并根据差值更新最小值
diff = val - self.minvalue
self.stack.append(diff)
if diff < 0:
self.minvalue = val
def pop(self) -> None:
temp = self.stack.pop()
# 栈顶元素小于0表示,当前栈顶元素就是最小值
# 所以需要更新之后的最小值
if temp<0:
self.minvalue = self.minvalue-temp
def top(self) -> int:
# 栈顶元素小于0表示,当前栈顶元素就是最小值
if self.stack[-1] < 0:
return self.minvalue
else:
# 大于0 则根据栈顶存储的差值与最小值来计算 实际栈顶元素值
return self.minvalue + self.stack[-1]
def getMin(self) -> int:
# 直接返回最小值即可
return self.minvalue
当栈为空的时候,更新最小值,差值0入栈。
if not self.stack:
self.stack.append(0)
self.minvalue = val
复杂度分析:
- 时间复杂度: O ( 1 ) O(1) O(1),所有操作都是常数时间复杂度。
- 空间复杂度: O ( 1 ) O(1) O(1),没有用到辅助栈等额外空间。