递归
阶乘
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
斐波那契数列
计算公式
def fibonacci(n):
if n <= 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
def main():
print(fibonacci(4)) # 5
if __name__ == "__main__":
main()
尾调用
def fibonacci(n, a=1, b=1):
if n == 0:
return a
if n == 1:
return b
return fibonacci(n - 1, b, a + b)
def main():
print(fibonacci(4)) # 5
if __name__ == "__main__":
main()
循环法
def fibonacci(n):
a, b = 1, 1
i = 1
while i <= n:
a, b = b, a + b
i = i + 1
return a
def main():
print(fibonacci(4)) # 5
if __name__ == "__main__":
main()
ackerman 函数
当一个函数以及它的一个变量是由函数本身定义时, 称为双递归函数
# 要求 m 和 n 是非负整数
def ackermann(n, m):
if n == 1 and m == 0:
return 2
elif n == 0:
return 1
elif m == 0 and n >= 2:
return n + 2
else:
return ackermann(ackermann(n - 1, m), m - 1)
def main():
# 2n
print(ackermann(1, 1)) # 2
print(ackermann(2, 1)) # 4
print(ackermann(3, 1)) # 6
# 2^n
print(ackermann(1, 2)) # 2
print(ackermann(2, 2)) # 4
print(ackermann(3, 2)) # 8
if __name__ == "__main__":
main()
整数划分
将正整数 n 表示成一系列正整数之和, 称为正整数 n 的划分, 记为
如正整数 6 存在 11 种划分
6 --> 最大加数 6
5 + 1 --> 最大加数 5
4 + 2, 4 + 1 + 1
3 + 3, 3 + 2 + 1, 3 + 1 + 1 + 1
2 + 2 + 2, 2 + 2 + 1 + 1, 2 + 1 + 1 + 1 + 1
1 + 1 + 1 + 1 + 1 + 1
计算最大加数不超过 m 的划分个数的递推公式为
def q(n, m):
if n == 1 or m == 1:
# 最大加数为 1 时, 只有一种划分, 即 1 + 1 + ...
return 1
elif n < m:
# 最大加数不可以大于 n
return q(n, n)
elif n == m:
# 最大加数是自身时, 有一个划分为 n, 剩下的为正整数 n - 1 的划分
return 1 + q(n, n - 1)
else:
return q(n, m - 1) + q(n - m, m)
def main():
print(q(6, 6)) # 11
if __name__ == "__main__":
main()
汉诺塔问题
def hanoi(n, source, target, auxiliary):
if n == 1:
print(f"Move disk 1 from {source} to {target}")
else:
# 先把 n - 1 个盘子移到 auxiliary
hanoi(n - 1, source, auxiliary, target)
# 把最后一个盘子移到 target
print(f"Move disk {n} from {source} to {target}")
# 再把 n - 1 个盘子从 auxiliary 移到 target
hanoi(n - 1, auxiliary, target, source)
def main():
# Move disk 1 from a to b
# Move disk 2 from a to c
# Move disk 1 from b to c
# Move disk 3 from a to b
# Move disk 1 from c to a
# Move disk 2 from c to b
# Move disk 1 from a to b
hanoi(3, "a", "b", "c")
if __name__ == "__main__":
main()
分治策略
二分搜索
最坏情况下时间复杂度
解得
def binarySearch(a, x):
left = 0
right = len(a)
while left <= right:
mid = (left + right) // 2 # 取整
if x == a[mid]:
return mid
elif x > a[mid]:
left = mid + 1
else:
right = mid - 1
return -1
def main():
arr = [1, 2, 3, 4, 5, 6, 7]
print(binarySearch(arr, 6)) # 5
if __name__ == "__main__":
main()
归并排序
最坏情况下时间复杂度
解得
def merge(a, left, mid, right):
temp = list(a) # 拷贝原数组
i = left
j = mid + 1
k = i
while i <= mid and j <= right:
# 根据大小将 temp 拷贝回 a
if a[i] <= a[j]:
a[k] = temp[i]
i += 1
else:
a[k] = temp[j]
j += 1
k += 1
# 剩余未比较部分拷贝回原数组
while i <= mid:
a[k] = temp[i]
k += 1
i += 1
while j <= right:
a[k] = temp[j]
k += 1
j += 1
def mergeSort(a, left, right):
if left < right:
mid = (left + right) // 2
mergeSort(a, left, mid)
mergeSort(a, mid + 1, right)
merge(a, left, mid, right)
def main():
arr = [1, 3, 5, 2, 8, 6, 9]
mergeSort(arr, 0, len(arr) - 1)
print(arr) # [1, 2, 3, 5, 6, 8, 9]
if __name__ == "__main__":
main()
快速排序
最坏情况(刚好完全逆序的时候)
解得
最好的情况(基准点刚好为中值)
解得
挖坑法
def partition(a, p, r):
i = p
j = r
x = a[p] # 基准点
while i < j:
# 从后往前, 大于基准就跳过
while i < j and a[j] >= x:
j -= 1
if i < j:
# 当不大于基准点时与左边元素交换
a[i] = a[j]
i += 1
# 从前往后, 小于基准点跳过
while i < j and a[i] < x:
i += 1
if i < j:
# 当不小于基准点时与右边交换
a[j] = a[i]
j -= 1
# 基准点放置在某点, 使其左边均比其小, 右边均比其大
a[i] = x
return i
def qSort(a, p, r):
if p < r:
q = partition(a, p, r)
qSort(a, p, q - 1)
qSort(a, q + 1, r)
def main():
arr = [1, 3, 2, 6, 4, 8, 5, 0]
qSort(arr, 0, len(arr) - 1)
print(arr) # [0, 1, 2, 3, 4, 5, 6, 8]
if __name__ == "__main__":
main()
Hoare 方法
def partition(a, p, r):
i = p
j = r
x = a[p] # 基准点
while i < j:
while i < j and a[j] >= x:
j -= 1
while i < j and a[i] <= x:
i += 1
a[i], a[j] = a[j], a[i] # 交换两个元素
# 最后基准点与 a[i] 交换
a[p], a[i] = a[i], a[p]
return i
def qSort(a, p, r):
if p < r:
q = partition(a, p, r)
qSort(a, p, q - 1)
qSort(a, q + 1, r)
def main():
arr = [1, 3, 2, 6, 4, 8, 5, 0]
qSort(arr, 0, len(arr) - 1)
print(arr) # [0, 1, 2, 3, 4, 5, 6, 8]
if __name__ == "__main__":
main()
芯片测试
条件:有n 片芯片(好芯片至少比坏芯片多 1 片)
问题:使用最少测试次数, 从中挑出 1 片好芯片
两两一组测试, 淘汰后芯片进入下一轮, 如果测试结果是情况 1, 那么 A、B 中留 1 片, 丢 1 片, 如果是后三种情况, 则把 A 和 B 全部丢掉
快速幂
时间复杂度为
def fast_power(base, exponent):
if exponent == 0:
return 1
b = base * base
if exponent % 2 == 0:
return fast_power(b, exponent // 2)
else:
return base * fast_power(b, (exponent - 1) // 2)
def main():
print(fast_power(2, 10)) # 1024
if __name__ == "__main__":
main()
大整数乘法
普通大整数乘法为
共需要 4 次乘法(AC、AD、BC、BD)和 3 次加法, 2 次移位运算
解得
为了减少计算次数, 将乘积改为
这样只需要做 3 次乘法(AC、BD、(A-B)(D-C)), 6 次加减法, 2 次移位运算
解得
循环赛日程表
- 每个选手与其他选手各比赛一次
- 每个选手一天只能比赛一次
- 比赛一共进行 n - 1 天
def table(k):
n = 2**k # 选手数
res = [[0 for _ in range(n)] for _ in range(n)] # 日程表
for i in range(n):
# 第一个选手与其他选手按顺序对弈
res[0][i] = i + 1
m = 1
for _ in range(k):
n //= 2
for t in range(n):
for i in range(m, 2 * m):
for j in range(m, 2 * m):
res[i][j + t * m * 2] = res[i - m][j + t * m * 2 - m]
res[i][j + t * m * 2 - m] = res[i - m][j + t * m * 2]
m *= 2
return res
def main():
t = table(3) # 2^3=8 个选手
# [1, 2, 3, 4, 5, 6, 7, 8]
# [2, 1, 4, 3, 6, 5, 8, 7]
# [3, 4, 1, 2, 7, 8, 5, 6]
# [4, 3, 2, 1, 8, 7, 6, 5]
# [5, 6, 7, 8, 1, 2, 3, 4]
# [6, 5, 8, 7, 2, 1, 4, 3]
# [7, 8, 5, 6, 3, 4, 1, 2]
# [8, 7, 6, 5, 4, 3, 2, 1]
for i in t:
print(i)
if __name__ == "__main__":
main()
选择问题
import random
# 生成随机数列
def random_list(n):
return [random.randint(1, 100) for _ in range(n)]
选最大
时间复杂度为
def find_max(a):
max = a[0]
k = 0
for idx, item in enumerate(a[1:]):
if max < item:
max = item
k = idx
return max, k
def main():
arr = random_list(10)
print(arr) # [60, 34, 19, 17, 32, 63, 71, 97, 97, 87]
print(find_max(arr)) # (97, 6)
if __name__ == "__main__":
main()
选最大最小
最坏时间复杂度(先找最大, 再从剩下的找最小)
def find_max_min(a):
max_ = a[0]
min_ = a[0]
for item in a[1:]:
# 不用单独找最大最小, 两者能够同时进行
if item > max_:
max_ = item
elif item < min_:
min_ = item
return max_, min_
def main():
arr = random_list(10)
print(arr) # [4, 10, 85, 57, 2, 16, 53, 53, 39, 32]
print(find_max_min(arr)) # (85, 2)
if __name__ == "__main__":
main()
找第二大
最坏时间复杂度(先找最大的, 再从剩下的元素中找最大的)
双指针法
def find_second(a):
max_ = second = float("-inf")
for item in a:
if item > max_:
# item 比 max_ 大时
second = max_
max_ = item
elif max_ > item > second:
# item 处于 second 与 max_ 之间时
second = item
return second
def main():
arr = [62, 52, 65, 26, 69, 71, 45, 76, 51, 76]
print(find_second(arr)) # 71
if __name__ == "__main__":
main()
递归法
见习题八
锦标赛算法
思路: 两两分组比较, 大者进入下一轮, 并把淘汰者加入链表
class Node:
def __init__(self, val):
self.val = val
self.next = None
def insert(self, node):
# 链表采用头插的方式, 这样 next 就是相对 self 第二小的
node.next, self.next = self.next, node
def find_second(nums):
if len(nums) < 2:
return None
nodes = [Node(num) for num in nums]
n = len(nodes)
while n > 1:
winners = []
for i in range(0, n, 2):
# 两两一组
if i + 1 < n:
# 找到比较大的数, 被淘汰的加入对方的链表中
if nodes[i].val > nodes[i + 1].val:
nodes[i].insert(nodes[i])
winners.append(nodes[i])
else:
nodes[i + 1].insert(nodes[i])
winners.append(nodes[i + 1])
else:
winners.append(nodes[i])
nodes = winners
n = len(nodes)
return nodes[0].next.val
def main():
arr = [62, 52, 65, 26, 69, 71, 45, 51, 76]
print(find_second(arr)) # 71
if __name__ == "__main__":
main()
一般选择性
排序法
时间复杂度依赖于排序算法, 最好的时间复杂度为
def find_k(a, k):
a = sorted(a) # 升序排列
print(a) # [25, 28, 35, 37, 38, 55, 78, 78, 85, 90]
return a[k - 1]
def main():
arr = random_list(10)
print(arr) # [55, 35, 85, 90, 37, 28, 78, 25, 78, 38]
print(find_k(arr, 5)) # 38
if __name__ == "__main__":
main()
分治选择算法
将数组 5 个一组进行分组, 并找出各自的中位数
根据中位数的中位数(上图9)对数组进行划分, 小的放到 S1, 大的放到 S2
if (k == S1.size() + 1) {
// 第 k 小的刚好是中位数
return m;
} else if (k <= S1.size()) {
// 第 k 小的在 S1 里
return select(S1, k);
} else {
// 第 k 小的在 S2 里, 变为第 k - |S1| - 1 小
return select(S2, k - S1.size() - 1);
}
划分成 5 个一组后找出每组的中位数最坏情况为
将数组划分为 A、B、C、D 四个区域, 令 , 即 A 和 D 均有 个元素, C 和 B 均有 个元素
最坏情况下子问题有( 即总是需要查找 ACD 或 ABD )
使用递归树进行复杂度分析
代码见 github
最邻近点对
一维的最邻近点
先排好序, 取中点, 在左右两边找距离最近的点, 然后和交界处的两个点距离做对比
def cpair1D(a):
n = len(a)
if n < 2:
return float("inf")
a = sorted(a) # 对 a 进行排序
mid = n // 2 # 中位数坐标
d1 = cpair1D(a[:mid])
d2 = cpair1D(a[mid:])
# 处于交界处的两个点
p = a[mid - 1]
q = a[mid]
return min([d1, d2, q - p])
def main():
arr = [55, 35, 85, 90, 37, 28, 25, 78, 38]
print(cpair1D(arr)) # 1
if __name__ == "__main__":
main()
二维最邻近点
如果点数小于 3 , 直接两两计算距离, 然后再比较谁最小
如果点数大于 3 , 则对 x 轴进行排序, 选取中线, 根据 x 坐标将点分为左右两边, 找到左右两边的最近距离
然后再考虑在中心线附近的点
这个范围为左边点附近 2d × d 矩形, 并且每个邻近点最多与右边的 6 个点做比较
代码见 github
练习题
习题一: 二分查找
设 是已排好序的数组, 请改写二分搜索算法, 使得当搜索元素不在数组中时, 返回小于 x 的最大元素位置和大于 x 的最小元素位置 , 当搜索元素在数组中时, i 和 j 相同, 均为 x 在数组中的位置
function binarySearch(a: number[], x: number): number[] {
let middle: number;
let left = 0;
let right = a.length - 1;
while (left <= right) {
middle = Math.ceil((left + right) / 2);
if (x == a[middle]) {
return [middle, middle];
}
if (x > a[middle]) {
left = middle + 1;
} else {
right = middle - 1;
}
}
return [right, left];
}
// [3, 4]
console.log(binarySearch([0, 1, 2, 3, 5, 6], 4));
习题二: 大整数乘法
设计 n 位数字大整数 uv 的乘法时间复杂度 的算法
当 m 比 n 小得多时, 将 v 分成 段, 每段 m 位, 计算 uv 需要 次 m 位乘法运算, 利用分治算法, 每次 m 位d大整数乘法耗时 , 因此, 算法所需的计算时间为
习题三: 多项式计算
设 是一个 d 次多项式, 假设已有一个算法能在 时间内计算一个 i 次多项式与一个一次多项式的乘积, 以及一个算法能在 时间内计算 2 个 i 次多项式的乘积, 对于任给定的 d 个整数 , 用分治法设计一个有效算法算出满足 且最高次项系数为 1 的 d 次多项式 P(x), 并分析算法的效率
由于 且最高项为 ( x的系数为1 ), 因此多项式可以表示为
因此有递推公式
当 时, 即 时, 递归结束, 此时
设 n 个不同的整数排好序后存于 中, 若存在一个下标 i , , 使得 , 设计一个有效算法找到这个下标, 要求算法在最坏情况下的计算时间为
const arr = [-3, -2, 0, 3, 5, 6, 8, 9];
function binarySearch(a: number[]): number {
let left = 0;
let right = a.length - 1;
let mid: number;
// 二分查找
while (left <= right) {
mid = Math.ceil((left + right) / 2);
if (mid === a[mid]) return mid;
else if (mid > a[mid]) left = mid + 1;
else right = mid - 1;
}
return -1;
}
console.log(binarySearch(arr)); // 3
习题四: 确定主元素
设 是 个元素的数组, 对任一元素, 设 , 当 时, 称 i 为 T 的主元素, 设计一个
线性时间算法, 确定 是否有一个主元素( 出现次数超过一半 )
function majority_element(a: number[]) {
let candidate: number = 0;
let count = 0;
// 摩尔投票法
// 主元素由于数量大于一半, 所以最后相互抵消过程后必大于 0
for (let i = 0; i < a.length; i++) {
if (count === 0) {
candidate = a[i];
count = 1;
} else if (a[i] === candidate) {
count++;
} else {
count--;
}
}
return candidate;
}
console.log(majority_element([0, 10, 1, 1, 1, 1, 1, 9, 9, 2]));
习题五: 交换两个数组
设 是一个有 n 个元素的数组, 是一个非负整数, 设计一个算法将子数组 与 换位要求算法在坏情况下耗时 且只用到 的辅助空间
// 1.循环换位法
function forward(a: number[], k: number) {
for (let i = 0; i < k; i++) {
let t = a[0]; // 保存第一个元素
for (let j = 1; j < a.length; j++) {
// 所有元素前移
a[j - 1] = a[j];
}
a[a.length - 1] = t; // 第一个元素插入最后一个元素上
}
}
function backward(a: number[], k: number) {
const n = a.length;
for (let i = k; i < n; i++) {
let t = a[n - 1];
for (let j = n - 1; j > 0; j--) {
a[j] = a[j - 1];
}
a[0] = t;
}
}
function swap(a: number[], k: number): number[] {
if (k > a.length - k) backward(a, k);
else forward(a, k);
return a;
}
console.log(swap([1, 2, 3, 4, 5, 6, 7, 8], 3));
// 2.三次反转法
function reverse(a: number[], i: number, j: number) {
while (i < j) {
let t = a[i];
a[i++] = a[j];
a[j--] = t;
}
}
function swap(a: number[], k: number): number[] {
reverse(a, 0, k - 1); // [3,2,1,4,5,6,7,8]
reverse(a, k, a.length - 1); // [3,2,1,8,7,6,5,4]
reverse(a, 0, a.length - 1); // [4,5,6,7,8,1,2,3]
return a;
}
console.log(swap([1, 2, 3, 4, 5, 6, 7, 8], 3));
习题六: 合并有序数组
子数组 和 已排好序 , 试设计一个合并 2 个子数组为排好序的数组 的算法, 要求算法在最坏情况下所用的计算时间为 ,且只用到 的辅助空间
// 1. 循环换位合并法
function binarySearch(a: number[], x: number, left: number, right: number) {
let mid: number = 0;
while (left <= right) {
mid = Math.ceil((left + right) / 2);
if (x === a[mid]) return mid;
if (x > a[mid]) left = mid + 1;
else right = mid - 1;
}
if (x > a[mid]) return mid;
else return mid - 1;
}
// 将 a[s:t] 所有元素循环右移 k 位, 最后的元素补到第一位
function shiftright(a: number[], s: number, t: number, k: number) {
for (let i = 0; i < k; i++) {
let temp = a[t];
for (let j = t; j > s; j--) {
a[j] = a[j - 1];
}
a[s] = temp;
}
}
// 合并 a[0, k - 1] 与 a[k, n - 1]
function mergefor(a: number[], k: number) {
let i = 0;
let j = k;
while (i < j && j < a.length) {
// 在 a[k:n - 1] 处查找 a[i] 的插入位置
let p = binarySearch(a, a[i], j, a.length - 1);
// a[i:p] 右移 j ~ p 的个数
shiftright(a, i, p, p - j + 1);
j = p + 1;
i += p - j + 2;
}
return a;
}
console.log(mergefor([1, 2, 3, 6, 8, 4, 5, 7, 9], 5));
习题七: 找最大和最小
给定数组 , 试设计一个算法, 在最坏情况下用 次比较找出最大值和次大值
// 分治算法
function findMinMax(a: number[], start: number, end: number): [number, number] {
if (start === end) {
return [a[start], a[start]];
}
if (end - start === 1) {
if (a[start] > a[end]) {
return [a[end], a[start]];
}
return [a[start], a[end]];
}
let mid = Math.ceil((start + end) / 2);
const [lmin, lmax] = findMinMax(a, start, mid);
const [rmin, rmax] = findMinMax(a, mid + 1, end);
const min = lmin < rmin ? lmin : rmin;
const max = lmax > rmax ? lmax : rmax;
return [min, max];
}
console.log(findMinMax([1, 3, 5, 2, 7, 8, 9, 0, 10, -1], 0, 9));
习题八: 找最大和次大
给定数组 , 试设计一个算法, 在最坏情况下用 次比较找出 中元素的最大值和次大值
type rt = [number, number | undefined];
// 分治算法
function findMaxSecond(a: number[], start: number, end: number): rt {
if (start === end) {
return [a[start], undefined];
}
if (end - start === 1) {
return [Math.max(a[start], a[end]), Math.min(a[start], a[end])];
}
let mid = Math.ceil((start + end) / 2);
const [max1, second1] = findMaxSecond(a, start, mid);
const [max2, second2] = findMaxSecond(a, mid + 1, end);
let max: number, second: number;
if (max1 > max2) {
max = max1;
if (typeof second1 !== "undefined" && second1 > max2) {
second = second1;
} else {
second = max2;
}
} else {
max = max2;
if (typeof second2 !== "undefined" && second2 > max1) {
second = second2;
} else {
second = max1;
}
}
return [max, second];
}
console.log(findMaxSecond([1, 3, 5, 2, 7, 8, 9, 0, 10, -1], 0, 9));
习题九: 快排优化
试说明如何修改快速排序算法, 使它在最坏情况下的计算时间为
采用 三数取中 的方法选取基准点
function medianOfThree(arr, start, end) {
let mid = Math.floor((start + end) / 2);
// 对数组的左端点、中间点、右端点进行排序
if (arr[start] > arr[mid]) {
[arr[start], arr[mid]] = [arr[mid], arr[start]];
}
if (arr[mid] > arr[end]) {
[arr[mid], arr[end]] = [arr[end], arr[mid]];
if (arr[start] > arr[mid]) {
[arr[start], arr[mid]] = [arr[mid], arr[start]];
}
}
// 返回中间点的值作为基准元素
return arr[mid];
}