递归与分治策略

174 阅读7分钟

递归

阶乘

n!={1n=0n(n1)!n>1n! = \begin{cases} 1 & n = 0 \\ \\ n(n-1)! & n \gt 1 \end{cases}
def factorial(n):
  if n == 0:
      return 1

  return n * factorial(n - 1)

斐波那契数列

F(n)={1n=0,1F(n1)+F(n2)n2F(n) = \begin{cases} 1 & n=0,1 \\ \\ F(n-1)+F(n-2) & n \ge 2 \end{cases}

计算公式

F(n)=15[(1+52)n+1(152)n+1]F(n) = \frac{1}{\sqrt 5}\left[\left(\frac{1+\sqrt5}{2}\right)^{n+1} - \left(\frac{1-\sqrt 5}{2}\right)^{n+1}\right]
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 函数

当一个函数以及它的一个变量是由函数本身定义时, 称为双递归函数

{A(1,0)=2A(0,m)=1m0A(n,0)=n+2n2A(n,m)=A(A(n1,m),m1)n,m1\begin{cases} A(1,0)=2 \\\\ A(0,m)=1 & m\ge0 \\\\ A(n,0)=n+2 & n\ge2\\\\ A(n,m)=A(A(n-1,m), m-1) & n,m\ge1 \end{cases}
# 要求 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 的划分, 记为 p(n)p(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 的划分个数的递推公式为

q(n,m)={1n=1,m=1q(n,m)n<m1+q(n,n1)n=mq(n,m1)+q(nm,m)n>m>1q(n, m) = \begin{cases} 1 & n = 1, m = 1 \\ \\ q(n,m) & n < m \\ \\ 1 + q(n, n-1) & n = m \\ \\ q(n, m-1) + q(n-m, m) & n > m > 1 \end{cases}
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()

分治策略

二分搜索

最坏情况下时间复杂度

W(n)=W(n2)+1,W(1)=1W(n) = W\left(\left\lfloor\frac{n}{2}\right\rfloor\right)+1, W(1) = 1

解得

W(n)=logn+1W(n)=\lfloor logn \rfloor + 1
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()

归并排序

T(n)={O(1)n12T(n/2)+O(n)n>1 T(n) = \begin{cases} O(1) & n \le 1 \\ \\ 2T(n/2) + O(n) & n > 1 \end{cases}

最坏情况下时间复杂度

W(n)=2W(n2)+n1,W(1)=0W(n) = 2W\left(\frac{n}{2}\right) + n - 1, W(1) = 0

解得

W(n)=nlognn+1W(n)=nlogn -n + 1
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()

快速排序

最坏情况(刚好完全逆序的时候)

W(n)={0n=1W(n1)+n1n>1W(n)= \begin{cases} 0 & n = 1\\ \\ W(n-1) + n - 1 & n \gt 1 \end{cases}

解得

W(n)=12n(n1)=O(n2)W(n) = \frac{1}{2}n(n-1)=O(n^2)

最好的情况(基准点刚好为中值)

T(n)={0n=12T(n2)+n1n>1T(n)= \begin{cases} 0 & n = 1\\ \\ 2T(\frac{n}{2}) + n - 1 & n \gt 1 \end{cases}

解得

T(n)=O(nlogn)T(n) = O(nlogn)

挖坑法

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 片好芯片

image.png

两两一组测试, 淘汰后芯片进入下一轮, 如果测试结果是情况 1, 那么 A、B 中留 1 片, 丢 1 片, 如果是后三种情况, 则把 A 和 B 全部丢掉

快速幂

an={an2×an2n=evenan12×an12×an=odda^n = \begin{cases} a^{\frac{n}{2}}×a^{\frac{n}{2}} & n = even \\\\ a^{\frac{n-1}{2}}×a^{\frac{n-1}{2}}×a & n = odd \end{cases}

时间复杂度为

T(n)=O(logn)T(n) = O(logn)
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()

大整数乘法

普通大整数乘法为

XY=(A2n2+B)(C2n2+D)=AC2n+(AD+CB)2n2+BDXY=(A2^{\frac{n}{2}}+B)(C2^{\frac{n}{2}}+D)=AC2^n+(AD+CB)2^{\frac{n}{2}}+BD

共需要 4 次乘法(AC、AD、BC、BD)和 3 次加法, 2 次移位运算

T(n)={O(1)n=14T(n/2)+O(n)n>1T(n) = \begin{cases} O(1) & n = 1 \\\\ 4T(n/2) + O(n) & n > 1 \end{cases}

解得

T(n)=O(n2)T(n)=O(n^2)

为了减少计算次数, 将乘积改为

XY=AC2n+((AB)(DC)+AC+BD)2n2+BDXY=AC2^n+((A-B)(D-C)+AC+BD)2^{\frac{n}{2}}+BD

这样只需要做 3 次乘法(AC、BD、(A-B)(D-C)), 6 次加减法, 2 次移位运算

T(n)={O(1)n=13T(n/2)+O(n)n>1T(n) = \begin{cases} O(1) & n = 1 \\\\ 3T(n/2) + O(n) & n > 1 \end{cases}

解得

O(nlog3)=O(n1.59)O\left(n^{log3}\right)=O(n^{1.59})

循环赛日程表

  • 每个选手与其他选手各比赛一次
  • 每个选手一天只能比赛一次
  • 比赛一共进行 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)]

选最大

时间复杂度为

T(n)=n1=O(n)T(n) = n - 1 = O(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()

选最大最小

最坏时间复杂度(先找最大, 再从剩下的找最小)

W(n)=2n3=O(n)W(n)=2n-3=O(n)
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()

找第二大

最坏时间复杂度(先找最大的, 再从剩下的元素中找最大的)

W(n)=2n3=O(n)W(n) = 2n-3=O(n)

双指针法

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()

一般选择性

排序法

时间复杂度依赖于排序算法, 最好的时间复杂度为

O(nlogn)O(nlogn)
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 个一组进行分组, 并找出各自的中位数

image.png

根据中位数的中位数(上图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 个一组后找出每组的中位数最坏情况为

W(n5)W\left(\frac{n}{5}\right)

image.png

将数组划分为 A、B、C、D 四个区域, 令 n=5(2r+1)n=5(2r+1), 即 A 和 D 均有 2r2r 个元素, C 和 B 均有 3r+23r+2 个元素

最坏情况下子问题有( 即总是需要查找 ACD 或 ABD )

2r+2r+3r+2=7r+22r + 2r + 3r + 2 = 7r + 2
W(7r+2)=W(7(n1012)+2)=W(7n1032)W(7n10)W(7r+2)=W\left(7\left(\frac{n}{10}-\frac{1}{2}\right) + 2\right) = W\left(\frac{7n}{10}-\frac{3}{2}\right) \le W\left(\frac{7n}{10}\right)

使用递归树进行复杂度分析

image.png

w(n)W(n5)+W(7n10)+tntn+0.9tn+0.81tn+=O(n)w(n)\le W\left(\frac{n}{5}\right) + W\left(\frac{7n}{10}\right) + tn \le tn + 0.9tn + 0.81tn + \cdots=O(n)

代码见 github

image.png

最邻近点对

一维的最邻近点

先排好序, 取中点, 在左右两边找距离最近的点, 然后和交界处的两个点距离做对比

image.png

T(n)={O(1)n<42T(n/2)+O(n)n4T(n) = \begin{cases} O(1) & n < 4 \\\\ 2T(n/2) + O(n) & n \ge 4 \end{cases}
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 坐标将点分为左右两边, 找到左右两边的最近距离

image.png

然后再考虑在中心线附近的点

image.png

这个范围为左边点附近 2d × d 矩形, 并且每个邻近点最多与右边的 6 个点做比较

代码见 github

练习题

习题一: 二分查找

a[0:n1]a[0:n-1] 是已排好序的数组, 请改写二分搜索算法, 使得当搜索元素不在数组中时, 返回小于 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 的乘法时间复杂度 O(nm32)O\left(nm^\frac{3}{2}\right) 的算法

当 m 比 n 小得多时, 将 v 分成 nm\frac{n}{m} 段, 每段 m 位, 计算 uv 需要 nm\frac{n}{m} 次 m 位乘法运算, 利用分治算法, 每次 m 位d大整数乘法耗时 O(mlog3)O\left(m^{log3}\right) , 因此, 算法所需的计算时间为 O(nmmlog3)=O(nmlog32)O\left(\frac{n}{m}·m^{log3}\right)=O\left(nm^{log\frac{3}{2}}\right)

习题三: 多项式计算

P(x)=a0+a1++adxdP(x)=a_0 + a_1 + \cdots + a_dx^d 是一个 d 次多项式, 假设已有一个算法能在 O(i)O(i) 时间内计算一个 i 次多项式与一个一次多项式的乘积, 以及一个算法能在 O(ilogi)O(ilogi) 时间内计算 2 个 i 次多项式的乘积, 对于任给定的 d 个整数 n1,n2,,ndn_1, n_2, \cdots, n_d, 用分治法设计一个有效算法算出满足 P(n1)=P(n2)==P(nd)=0P(n_1)=P(n_2)=\cdots=P(n_d)=0 且最高次项系数为 1 的 d 次多项式 P(x), 并分析算法的效率

由于 P(ni)=0P(n_i) = 0 且最高项为 11 ( x的系数为1 ), 因此多项式可以表示为

P(x)=i=1d(xni)=i=1d/2(xni)id/21d(xni)=P1(x)P2(x)P(x) = \prod_{i=1}^d(x-n_i)=\prod_{i=1}^{\lceil d/2 \rceil}(x-n_i)\prod_{i-\lceil d/2 \rceil - 1}^d (x - n_i) = P_1(x)P_2(x)

因此有递推公式

T(d)={O(1)d=12T(d/2)+O(dlogd)d>1T(d) = \begin{cases} O(1) & d = 1 \\ 2T(d/2) + O(d\log d) & d > 1 \end{cases}
T(d)=2T(d/2)+O(dlogd)=2(2T(d/4)+O(d/2log(d/2)))+O(dlogd)=22T(d/22)+2O(d/2log(d/2))+O(dlogd)=2kT(d/2k)+kO(dlogd)\begin{aligned} T(d) &= 2T(d/2) + O(d\log d) \\ &= 2(2T(d/4) + O(d/2\log(d/2))) + O(d\log d) \\ &= 2^2T(d/2^2) + 2O(d/2\log(d/2)) + O(d\log d) \\ &\vdots \\ &= 2^kT(d/2^k) + kO(d\log d) \end{aligned}

d/2k=1d/2^k=1 时, 即 k=logdk=\log d 时, 递归结束, 此时 T(d)=O(dlog2d)T(d)=O(d\log^2d)

设 n 个不同的整数排好序后存于 T[i:n]T[i:n] 中, 若存在一个下标 i , 0i<n0 \le i \lt n, 使得 T[i]=iT[i]=i, 设计一个有效算法找到这个下标, 要求算法在最坏情况下的计算时间为 O(logn)O(logn)

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

习题四: 确定主元素

T[0:n]T[0:n]nn 个元素的数组, 对任一元素, 设 S(x)={iT[i]=x}S(x)=\{i|T[i]=x\} , 当 S(x)>n/2|S(x)|>n/2 时, 称 i 为 T 的主元素, 设计一个 线性时间算法 , 确定 T[0,n1]T[0,n-1] 是否有一个主元素( 出现次数超过一半 )

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]));

习题五: 交换两个数组

a[0:n1]a[0:n-1] 是一个有 n 个元素的数组, k(0kn1)k(0\le k \le n-1) 是一个非负整数, 设计一个算法将子数组 a[0:k1]a[0:k-1]a[k+1:n1]a[k+1:n-1] 换位要求算法在坏情况下耗时 O(n)O(n) 且只用到 O(1)O(1) 的辅助空间

// 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));

习题六: 合并有序数组

子数组 a[0:h1]a[0:h-1]a[k:n1]a[k:n-1] 已排好序 0kn10 ≤ k ≤ n—1 , 试设计一个合并 2 个子数组为排好序的数组 a[0:n1]a[0:n-1] 的算法, 要求算法在最坏情况下所用的计算时间为 O(n)O(n),且只用到 O(1)O(1) 的辅助空间

// 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));

习题七: 找最大和最小

给定数组 a[0:n1]a[0:n-1] , 试设计一个算法, 在最坏情况下用 [3n/22][3n/2-2] 次比较找出最大值和次大值

// 分治算法
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));

习题八: 找最大和次大

给定数组 a[0:n1]a[0:n-1] , 试设计一个算法, 在最坏情况下用 n+logn2n+\lceil logn \rceil -2 次比较找出 a[0:n1]a[0:n-1] 中元素的最大值和次大值

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));

习题九: 快排优化

试说明如何修改快速排序算法, 使它在最坏情况下的计算时间为 O(nlogn)O(nlogn)

采用 三数取中 的方法选取基准点


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];
}