数据结构与算法 - Rust 语言实现

使用 Rust 语言实现所有的数据结构与算法.

在线浏览

本文档包括了以下几个部分的内容:

  1. 第一部分: 数据结构
  2. 第二部分: 算法
  3. 第三部分: 专题
  4. 第四部分: leetcode 题解

反馈问题

欢迎 反馈问题, 或者提交 PR.

搭建本地环境

想在本地搭建本文档的环境也是很容易的, 这些文档记录以 markdown 文件为主, 用 mdbook 生成网页:

  1. 用cargo来安装它: cargo install mdbook mdbook-linkcheck mdbook-pagetoc
  2. 运行 mdbook build 命令, 会在book/目录里生成完整的电子书的网页版本.
  3. 使用 mdbook serve 命令监控文件变更, 并启动一个本地的 web 服务器, 在浏览器中打开 http://localhost:3000

生成 PDF

如果想生成 pdf, 需要安装 mdbook-pandoc:

  • 运行 ./tools/install-pdf-deps.sh 脚本安装相应的依赖包
  • 运行 ./tools/generate-pdf.sh 脚本, 就会生成 book-pandoc/pdf/TheAlgorithms.pdf

版权

文档采用 知识共享署名 4.0 国际许可协议 发布, 源代码依照 GPL 3.0 协议 发布.

数组 Arrays

数组是计算机科学中最基础最常用的一种数据结构, 它几乎无处不在.

数组是一种线程数据结构, 它可以存储一系列有着相同数据类型并占用相同内存大小的元素.

数组的特点有:

  • 它在内存中是一块连续的内存
  • 每个元素在数组中都占用一个唯一的独特的索引编号, 其中的第一元素编号是 0
  • 对其中的任意元素进行访问的时间一致, 支持随机访问

数组基本的布局如下图如示:

array layout

根据元素分布方式, 可以将数组分类为:

  • 一维数组 (one dimensional array), 数组中只存储一行元素, 这也是本章要介绍的
  • 多维数组 (multi-dimensional array), 数组中存储多行元素, 又称为 矩阵 matrix, 在后面章节有介绍

数组的基本操作

在 Rust 中, 数组的类型是 [T; N], 其中 T 是数组中存放的元素的数据类型, 而 N 表示数组的元素个数. 它们一旦被确定后就不能再更改. 另外, 在栈上存储的数组, 其占用的内存大小是在编译期间确定了的, 对栈上数组中的元素的访问效率极高.

访问元素

访问数组中的元素, 是通过该元素在数组中的索引值实现的, 该索引值是唯一的.

比如:

fn main() {
    let numbers: [i32; 6] = [1, 1, 2, 3, 5, 8];
    assert_eq!(numbers[0], 1);
    assert_eq!(numbers[3], 3);
    assert_eq!(numbers[5], 8);
}

交换数组中的两个元素

在进行数组排序时, 经常需要交换其中的元素, 其时间复杂度是 O(1):

fn main() {
    let mut numbers: [i32; 6] = [1, 1, 2, 3, 5, 8];
    assert_eq!(numbers[0], 1);
    numbers.swap(0, 5);
    assert_eq!(numbers[5], 1);
}

批量填充新的值

如果需要批量修改数组中的元素, 可以使用这个方法:

use std::sync::atomic::{AtomicI32, Ordering};

fn get_next_id() -> i32 {
    static NEXT_ID: AtomicI32 = AtomicI32::new(1);
    NEXT_ID.fetch_add(1, Ordering::Relaxed)
}

fn main() {
    let mut numbers = [1, 1, 2, 3, 5];
    numbers.fill(0);
    assert_eq!(numbers, [0, 0, 0, 0, 0]);
    numbers.fill_with(|| get_next_id().pow(2));
    assert_eq!(numbers, [1, 4, 9, 16, 25]);
}

搜索

有序数组和元序数组的搜索方式和性能差别很大, 我们放在了 单独的章节.

排序

数组排序的方法比较多, 我们放在排序 这一章单独介绍.

反转数组 Reverse

反转数组, 就是将数组中各元素转换到它相反的位置:

  • 第一个位置的元素移到最后一位
  • 第二个位置的元素移到倒数第二位
  • 依次类推

得到的结果如下图所示:

reverse array

操作过程

根据反转数组的描述, 使用靠拢型双指针法遍历整个数组, 并交换元素的值.

操作步骤如下图所示:

reverse array steps

代码实现

#![allow(unused)]
fn main() {
pub fn reverse_array(arr: &mut [i32]) {
    if arr.len() < 2 {
        return;
    }

    let mut start = 0;
    let mut end = arr.len() - 1;
    while start < end {
        let tmp = arr[end];
        arr[end] = arr[start];
        arr[start] = tmp;
        start += 1;
        end -= 1;
    }
}

#[cfg(test)]
mod tests {
    use super::reverse_array;

    #[test]
    fn test_reverse_array() {
        let mut arr = [1, 2, 3, 4, 5];
        reverse_array(&mut arr);
        assert_eq!(arr, [5, 4, 3, 2, 1]);
    }
}
}

旋转数组 Rotate

给定一个数组, 包含 n 个元素, 要求将数组中的元素都依次向左移动 k 个位置. 如果 k 小于0, 就向右移动. 比如:

  • 输入: arr = [1, 2, 3, 4]; k = -2, 输出: arr = [3, 4, 1, 2]
  • 输入: arr = [1, 2, 3, 4]; k = 1, 输出: arr = [2, 3, 4, 1]

首先先将问题简化:

  • 如果向右移动 k 个位置, 其实就相当于向左移动了 n-k 个位置; 所以我们刚开始只需要考虑左移的问题
  • 如果向左移动了 c * n + k 个位置, 就相当于向左移动了 k 个位置, 因为经过 c * n 轮移动后, 元素位置并没有变化

方法1: 使用临时数组, 拷贝一份

操作过程如下:

  1. arr[k..n] 存储到临时数组
  2. arr[0..k] 存储到临时数组
  3. 将临时数组中的元素拷贝回原数组

这个方法的时间复杂度是 O(n), 空间复杂度是 O(n).

代码如下:

#![allow(unused)]
fn main() {
/// 使用临时数组
pub fn rotate_left_1(slice: &mut [i32], k: usize) {
    if slice.is_empty() {
        return;
    }

    let len = slice.len();
    let k = k % len;
    if k == 0 {
        return;
    }
    debug_assert!(k > 0 && k < len);

    let mut tmp: Vec<i32> = Vec::with_capacity(len);
    // 复制第一部分
    for &num in &slice[k..] {
        tmp.push(num);
    }

    // 复制第二部分
    for &num in &slice[..k] {
        tmp.push(num);
    }

    // 写回到原数组
    for (i, &num) in tmp.iter().enumerate() {
        slice[i] = num;
    }
}

/// 支持向右旋转
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_sign_loss)]
pub fn rotate_array_1(slice: &mut [i32], k: isize) {
    let len = slice.len() as isize;
    if len == 0 {
        return;
    }
    let quot: isize = k / len;
    let k = if k < 0 { (1 - quot) * len + k } else { k };

    let k = k as usize;
    rotate_left_1(slice, k);
}
}

方法2: 三次反转法

操作过程如下:

  1. arr[k..n] 进行反转
  2. arr[0..k] 进行反转
  3. arr[..] 进行反转

这个方法是在原地操作的, 其时间复杂度是 O(n), 空间复杂度是 O(1).

流程如下图所示:

array rotate with reversal

代码如下:

#![allow(unused)]
fn main() {
/// 原地反转数组
pub fn rotate_left_2(slice: &mut [i32], k: usize) {
    if slice.is_empty() {
        return;
    }

    let len = slice.len();
    let k = k % len;
    if k == 0 {
        return;
    }
    debug_assert!(k > 0 && k < len);

    slice[k..len].reverse();
    slice[..k].reverse();
    slice.reverse();
}

/// 支持向右旋转
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_sign_loss)]
pub fn rotate_array_2(slice: &mut [i32], k: isize) {
    let len = slice.len() as isize;
    if len == 0 {
        return;
    }
    let quot: isize = k / len;
    let k = if k < 0 { (1 - quot) * len + k } else { k };

    let k = k as usize;
    rotate_left_2(slice, k);
}
}

方法3: 一步到位

所谓的一步到位法, 就是先计算好每个元素在旋转后的新位置, 然后依次转移每一个元素, 一步到位; 每个元素只移动一次.

操作过程如下:

  1. 计算数组中元素个数 n 与偏移量 k 的最大公约数 divisor
  2. 然后从 0 循环到 divisor, 把数组中的元素分成以 k 为步长, 组成的集合; 如果索引值超过了数组长度, 就取余
  3. 在循环体内部, 将集合中的第一个元素存到临时变量
  4. 依次将集合中的后一元素移动前一个元素
  5. 将临时变量存储到集合中的最后一个元素
  6. 最终将该集合中所有元素依次移位

这个方法是在原地操作的, 其时间复杂度是 O(n), 空间复杂度是 O(1).

流程如下图所示:

array rotate with juggling

#![allow(unused)]
fn main() {
#[must_use]
pub fn gcd(mut a: usize, mut b: usize) -> usize {
    debug_assert!(a > 0 && b > 0);
    while a != b {
        (a, b) = if a > b { (a - b, b) } else { (b - a, a) }
    }
    a
}

/// 一步到位
pub fn rotate_left_3(slice: &mut [i32], k: usize) {
    if slice.is_empty() {
        return;
    }

    let len = slice.len();
    let k = k % len;
    if k == 0 {
        return;
    }
    debug_assert!(k > 0 && k < len);

    // 第一步: 计算最大公约数
    let divisor = gcd(k, len);

    // 第二步: 从0遍历到最大公约数, 分隔成多个子集
    for i in 0..divisor {
        // 遍历每个子集中的元素, 依次移位
        // 先将集合中的第一个元素存到临时变量
        let tmp = slice[i];
        let mut head = i;
        loop {
            let next = (head + k) % len;
            if next == i {
                break;
            }
            // 依次将集合中的后一个元素移到前一个元素所有位置
            slice[head] = slice[next];
            head = next;
        }
        // 最后临时变量的值存到集合中最后一个元素
        slice[head] = tmp;
    }
}

/// 支持向右旋转
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_sign_loss)]
pub fn rotate_array_3(slice: &mut [i32], k: isize) {
    let len = slice.len() as isize;
    if len == 0 {
        return;
    }
    let quot: isize = k / len;
    let k = if k < 0 { (1 - quot) * len + k } else { k };

    let k = k as usize;
    rotate_left_3(slice, k);
}
}

前缀和数组 Prefix Sum Array

什么是前缀和数组? prefix_sum_array[i] = prefix_sum_array[i - 1] + arr[i],

上面的定义不好理解的话, 我们再看一下例子, 原数组是 arr[] = [1, 2, 3, 4, 5];, 则前缀和数组就是: prefix_sum = [1, 3, 6, 10, 15];.

前缀和数组的算法倒是蛮简单, 如下所示:

#![allow(unused)]
fn main() {
use std::ops::Add;

pub fn prefix_sum<T>(arr: &[T]) -> Vec<T>
where
    T: Clone + Add<T, Output=T>,
{
    if arr.is_empty() {
        return vec![];
    }
    let mut list = Vec::with_capacity(arr.len());
    list.push(arr[0].clone());
    for i in 1..arr.len() {
        list.push(arr[i].clone() + list[i - 1].clone());
    }
    debug_assert!(list.len() == arr.len());
    list
}
}

该算法的时间复杂度是 O(n), 空间复杂度是 O(n).

这种算法思想主要是用于缓存某些需要频繁计算的过程, 以空间换取时间.

前缀和数组的应用

给定一个数组 arr, 计算 arr[l]arr[r] 之间的所有元素之和.

频繁的计算数组的部分连续项之和时, 每次计算都要从头算. 我们可以用前缀和数组, 这样每次计算时可以立即得到结果.

有下面的公式:

arr[left..=right].sum() = prefix_sum_array[right] - prefix_sum_array[left - 1];

算法实现如下:

fn prefix_sum(numbers: &[i32]) -> Vec<i32> {
    let mut accum = 0;
    let mut ps = Vec::with_capacity(numbers.len());
    for num in numbers {
        accum += num;
        ps.push(accum);
    }
    ps
}

fn main() {
    let arr = [8, 19, 28, 21, 33, 97, 62, 7, 45];
    let ps = prefix_sum(&arr);
    for left in 0..2 {
        for right in 3..arr.len() {
            let sum = if left == 0 {
                ps[right]
            } else {
                ps[right] - ps[left - 1]
            };
            let sum_slice = arr[left..=right].iter().sum();
            assert_eq!(sum, sum_slice);
        }
    }
}

Suffix Array

参考

矩阵 Matrix

矩阵的常用操作

稀疏矩阵

矩阵是由 m 行和 n 列组成的二维数据对象, 因此总共有 m x n 个值. 如果矩阵的大多数元素都有 0 值, 则称为稀疏矩阵 sparse matrix.

我们可以只存储稀疏矩阵中的非 0 元素, 这样做的好处有:

  • 存储空间: 非零元素的数量少于零元素的数量, 因此可以使用较少的内存来存储这些元素
  • 计算时间: 通过逻辑设计仅遍历非零元素的数据结构可以节省计算时间

用二维数组表示稀疏矩阵会导致大量内存浪费, 因为矩阵中的零在大多数情况下都是无用的. 因此我们只存储非零元素, 而不是将零与非零元素一起存储. 这意味着用三元组 (行, 列, 值) 存储非零元素.

稀疏矩阵表示可以通过多种方式完成, 以下是两种常见的表示形式:

  • 数组表示
  • 链表表示

接下来的章节将分别对这两类表示形式展示详细的说明.

数组

数组表示法来存储稀疏矩阵, 就是只在数组中存储里面非零的元素.

数组中每个元素项都包含三部分:

  • 该元素在矩阵中的行号
  • 该元素在矩阵中的列号
  • 该元素的值

比如:

\begin{bmatrix} \ 0 & 0 & 3 & 0 & 4 \\ 0 & 0 & 5 & 7 & 0 \\ 0 & 0 & 0 & 0 & 0 \\ 0 & 2 & 6 & 0 & 0 \ \end{bmatrix}

这个矩阵用数组存放, 效果如下图:

array sparse matrix

这种存储方式的特点是:

  • Row major 风格
  • 数组中元素的排序方法是
    • 从头到尾以行编号递增
    • 相同行编号时, 以列编号递增
    • 即整体上行编号有序递增, 整体上列编号无序, 但局部上列编号递增
  • 查找矩阵中某个节点的值时的性能是 O(log(m) * log(n)), 其中 mn 是矩阵中非 0 元素的最大行列数, 因为是有序排列的, 可以用二分查找法
  • 比较适合存放固定不变的矩阵, 插入或者删除元素的成本比较高

算法的实现

因为数组支持随机索引, 而且都是有序存储的, 向其中插入和移除元素的操作都比较快.

#![allow(unused)]
fn main() {
use std::{fmt, mem};
use std::cmp::Ordering;

use crate::traits::IsZero;

/// Each element node in the array.
pub struct MatrixElement<T: IsZero> {
    /// Row number of element.
    pub row: usize,
    /// Column number of element.
    pub column: usize,
    /// Value of element.
    pub value: T,
}

/// Store sparse matrix with array.
pub struct ArraySparseMatrix<T: IsZero> {
    vec: Vec<MatrixElement<T>>,
}

impl<T: IsZero> ArraySparseMatrix<T> {
    #[must_use]
    pub fn construct<I, I2>(sparse_matrix: I) -> Self
    where
        I: IntoIterator<Item=I2>,
        I2: IntoIterator<Item=T>,
    {
        let mut vec = Vec::new();

        for (row, row_list) in sparse_matrix.into_iter().enumerate() {
            for (column, element) in row_list.into_iter().enumerate() {
                if element.is_not_zero() {
                    let element = MatrixElement {
                        row,
                        column,
                        value: element,
                    };
                    vec.push(element);
                }
            }
        }
        Self { vec }
    }

    #[must_use]
    #[inline]
    pub fn len(&self) -> usize {
        self.vec.len()
    }

    #[must_use]
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.vec.is_empty()
    }

    fn find_element(&self, row: usize, column: usize) -> Result<usize, usize> {
        self.vec.binary_search_by(|node| {
            match node.row.cmp(&row) {
                Ordering::Equal => node.column.cmp(&column),
                order => order
            }
        })
    }

    /// Get node value at (row, column).
    #[must_use]
    pub fn value(&self, row: usize, column: usize) -> Option<T> {
        let result = self.find_element(row, column);
        result.ok().map(|index| self.vec[index].value)
    }

    /// Get mutable reference to node value at (row, column).
    #[must_use]
    pub fn value_mut(&mut self, row: usize, column: usize) -> Option<&mut T> {
        let result = self.find_element(row, column);
        result.ok().map(|index| &mut self.vec[index].value)
    }

    /// If found old node at (row, column), returns old value; otherwise returns None.
    pub fn add_element(&mut self, row: usize, column: usize, mut value: T) -> Option<T> {
        let result = self.find_element(row, column);

pub trait IsZero: Copy {
    fn is_zero(self) -> bool;
    fn is_not_zero(self) -> bool {
        !self.is_zero()
    }
}

macro_rules! impl_is_zero {
    ($($t:ty)*) => {$(
    impl IsZero for $t {
        fn is_zero(self) -> bool {
            self == 0
        }
    }
    )*}
}

impl_is_zero! { i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize }

impl IsZero for f32 {
    fn is_zero(self) -> bool {
        self == 0.0
    }
}

impl IsZero for f64 {
    fn is_zero(self) -> bool {
        self == 0.0
    }
}
}

链表

上文介绍了使用数组存储稀疏矩阵的方法, 但该方法不适合动态地插入和删除元素. 我们可以换成链表来存储, 链表上删减节点的操作很灵活.

链表中每个节点项都包含这几部分:

  • 该元素在矩阵中的行号
  • 该元素在矩阵中的列号
  • 该元素的值
  • 指向下个节点的指针

同样的矩阵:

\begin{bmatrix} \ 0 & 0 & 3 & 0 & 4 \\ 0 & 0 & 5 & 7 & 0 \\ 0 & 0 & 0 & 0 & 0 \\ 0 & 2 & 6 & 0 & 0 \ \end{bmatrix}

用链表来记录的话, 其结构如下图所示:

linked list sparse matrix

这种存储方式的特点是:

  • 同样是Row major 风格
  • 链表中节点的排序方法是
    • 从头到尾以行编号递增
    • 相同行编号时, 以列编号递增
    • 即整体上行编号有序递增, 整体上列编号无序, 但局部上列编号递增
  • 可以在任意位置插入或者移除节点
  • 查找元素的效率很低, 因为链表不支持随机访问, 只能从头到尾依次遍历. 其时间复杂度是 O(n), n 是矩阵中非 0 节点的个数

算法的实现

为了节省功夫, 我们使用了标准库中的双链表 LinkedList<T>, 而不是上面提到的单链表的形式.

#![allow(unused)]
fn main() {
pub trait IsZero: Copy {
    fn is_zero(self) -> bool;
    fn is_not_zero(self) -> bool {
        !self.is_zero()
    }
}

macro_rules! impl_is_zero {
    ($($t:ty)*) => {$(
    impl IsZero for $t {
        fn is_zero(self) -> bool {
            self == 0
        }
    }
    )*}
}

impl_is_zero! { i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize }

impl IsZero for f32 {
    fn is_zero(self) -> bool {
        self == 0.0
    }
}

impl IsZero for f64 {
    fn is_zero(self) -> bool {
        self == 0.0
    }
}

#![allow(dead_code)]

use std::fmt;
use std::marker::PhantomData;
use std::ptr::NonNull;

use crate::traits::IsZero;

/// Each element node in the linked list.
pub struct Node<T: IsZero> {
    /// Row number of element.
    pub row: usize,
    /// Column number of element.
    pub column: usize,
    /// Value of element.
    pub value: T,
    /// Pointer to next node.
    prev: NodePtr<T>,
    /// Pointer to next node.
    next: NodePtr<T>,
}

type NodePtr<T> = Option<NonNull<Node<T>>>;

/// Store sparse matrix with linked list.
#[allow(clippy::linkedlist)]
pub struct LinkedListSparseMatrix<T: IsZero> {
    len: usize,
    head: NodePtr<T>,
    tail: NodePtr<T>,
}

pub struct Iter<'a, T: 'a + IsZero> {
    head: NodePtr<T>,
    len: usize,
    _marker: PhantomData<&'a Node<T>>,
}

pub struct IterMut<'a, T: 'a + IsZero> {
    head: NodePtr<T>,
    len: usize,
    _marker: PhantomData<&'a mut Node<T>>,
}

impl<T: IsZero> LinkedListSparseMatrix<T> {
    #[must_use]
    pub fn construct<I, I2>(sparse_matrix: I) -> Self
    where
        I: IntoIterator<Item = I2>,
        I2: IntoIterator<Item = T>,
    {
        let mut head: NodePtr<T> = None;
        let mut tail: NodePtr<T> = None;
        let mut len: usize = 0;

        for (row, row_list) in sparse_matrix.into_iter().enumerate() {
            for (column, value) in row_list.into_iter().enumerate() {
                if value.is_not_zero() {
                    let mut node: NonNull<Node<T>> = Node::new_ptr(row, column, value);
                    len += 1;
                    if let Some(mut tail_ref) = tail {
                        unsafe {
                            node.as_mut().prev = tail;
                            tail_ref.as_mut().next = Some(node);
                        }
                    } else {
                        head = Some(node);
                    }
                    tail = Some(node);
                }
            }
        }
        Self { len, head, tail }
    }

    #[must_use]
    #[inline]
    pub const fn len(&self) -> usize {
        self.len
    }

    #[must_use]
    #[inline]
    pub const fn is_empty(&self) -> bool {
        self.len == 0
    }

    #[must_use]
    pub fn value(&self, row: usize, column: usize) -> Option<T> {
        for node in self {
            if node.row == row && node.column == column {
                return Some(node.value);
            }
            if node.row > row {
                return None;
            }
        }
        None
    }

    #[must_use]
    pub fn value_mut(&mut self, row: usize, column: usize) -> Option<&mut T> {
        for node in self.iter_mut() {
            if node.row == row && node.column == column {
                return Some(&mut node.value);
            }
            if node.row > row {
                return None;
            }
        }
        None
    }

    /// Add an element to the beginning of list.
    pub fn push_front(&mut self, row: usize, column: usize, value: T) {
        let node_ptr = Node::new_ptr(row, column, value);
        self.push_front_node(node_ptr);
    }

    /// Remove the first node in the list.
    pub fn pop_front(&mut self) -> Option<(usize, usize, T)> {
        self.pop_front_node().map(Node::into_inner)
    }

    pub fn push_back(&mut self, row: usize, column: usize, value: T) {
        let node_ptr = Node::new_ptr(row, column, value);
        self.push_back_node(node_ptr);
    }

    pub fn pop_back(&mut self) -> Option<(usize, usize, T)> {
        self.pop_back_node().map(Node::into_inner)
    }

    /// If found old node at (row, column), returns old value; otherwise returns None.
    #[must_use]
    pub fn add_element(&mut self, row: usize, column: usize, value: T) -> Option<T> {
        let len = self.len;
        for (index, node) in self.iter_mut().enumerate() {
            if node.row == row && node.column == column {
                let old_value = node.value;
                node.value = value;
                return Some(old_value);
            }
            if (node.row == row && node.column > column) || node.row > row {
                if index == 0 {
                    self.push_front(row, column, value);
                } else if index == len - 1 {
                    self.push_back(row, column, value);
                } else {
                    // Insert new node to previous of current node.
                    let new_node: NonNull<Node<T>> = Node::new_ptr(row, column, value);
                    unsafe { Self::insert_before(node, new_node) };
                    self.len += 1;
                }

                return None;
            }
        }

        // Add new node to end of list.
        self.push_back(row, column, value);
        None
    }

    /// If found node at (row, column), returns value of that node; otherwise returns None.
    #[must_use]
    pub fn remove_element(&mut self, row: usize, column: usize) -> Option<T> {
        let len = self.len;

        for (index, node) in self.iter_mut().enumerate() {
            if node.row == row && node.column == column {
                let value = node.value;
                if index == 0 {
                    self.pop_front();
                } else if index == len - 1 {
                    self.pop_back();
                } else {
                    unsafe {
                        Self::remove_node(node);
                    }
                    self.len -= 1;
                }
                return Some(value);
            }
            if (node.row == row && node.column > column) || node.row > row {
                // Node not found.
                return None;
            }
        }
        None
    }

    // Iterators
    #[must_use]
    pub const fn iter(&self) -> Iter<'_, T> {
        Iter {
            head: self.head,
            len: self.len,
            _marker: PhantomData,
        }
    }

    #[allow(clippy::needless_pass_by_ref_mut)]
    #[must_use]
    pub fn iter_mut(&mut self) -> IterMut<'_, T> {
        IterMut {
            head: self.head,
            len: self.len,
            _marker: PhantomData,
        }
    }
}

impl<T: IsZero> LinkedListSparseMatrix<T> {
    /// Insert `new_node` before `current_node`.
    unsafe fn insert_before(current_node_ref: &mut Node<T>, mut new_node: NonNull<Node<T>>) {
        if let Some(mut prev_node) = current_node_ref.prev {
            new_node.as_mut().prev = Some(prev_node);
            let current_node = prev_node.as_mut().next.take().unwrap();
            prev_node.as_mut().next = Some(new_node);

            new_node.as_mut().next = Some(current_node);
            current_node_ref.prev = Some(new_node);
        }
    }

    /// Insert `new_node` after `current_node`.
    unsafe fn insert_after(mut current_node: NonNull<Node<T>>, mut new_node: NonNull<Node<T>>) {
        if let Some(mut next_node) = current_node.as_mut().next {
            new_node.as_mut().next = Some(next_node);
            next_node.as_mut().prev = Some(new_node);
        }
        new_node.as_mut().prev = Some(current_node);
        current_node.as_mut().next = Some(new_node);
    }

    /// Remove `node` from list.
    ///
    /// Both prev and next node are valid.
    unsafe fn remove_node(node: &mut Node<T>) {
        let mut prev_node = node.prev.unwrap();
        let mut next_node = node.next.unwrap();
        prev_node.as_mut().next = Some(next_node);
        next_node.as_mut().prev = Some(prev_node);

        node.prev = None;
        node.next = None;
    }

    fn push_front_node(&mut self, node_ptr: NonNull<Node<T>>) {
        unsafe {
            (*node_ptr.as_ptr()).next = self.head;
            (*node_ptr.as_ptr()).prev = None;
        }
        let node = Some(node_ptr);

        match self.head {
            Some(head) => unsafe { (*head.as_ptr()).prev = node },
            None => self.tail = node,
        }

        self.head = node;
        self.len += 1;
    }

    fn push_back_node(&mut self, node_ptr: NonNull<Node<T>>) {
        unsafe {
            (*node_ptr.as_ptr()).next = None;
            (*node_ptr.as_ptr()).prev = self.tail;
        }
        let node = Some(node_ptr);

        match self.tail {
            Some(tail) => unsafe { (*tail.as_ptr()).next = node },
            None => self.head = node,
        }

        self.tail = node;
        self.len += 1;
    }

    fn pop_front_node(&mut self) -> Option<Box<Node<T>>> {
        self.head.map(|old_head| {
            let old_head = unsafe { Node::from_ptr(old_head) };
            self.head = old_head.next;

            match self.head {
                Some(head) => unsafe { (*head.as_ptr()).prev = None },
                None => self.tail = None,
            }

            self.len -= 1;
            old_head
        })
    }

    fn pop_back_node(&mut self) -> Option<Box<Node<T>>> {
        self.tail.map(|old_tail| {
            let old_tail = unsafe { Node::from_ptr(old_tail) };
            self.tail = old_tail.prev;

            match self.tail {
                Some(tail) => unsafe { (*tail.as_ptr()).next = None },
                None => self.head = None,
            }

            self.len -= 1;
            old_tail
        })
    }
}

impl<T: fmt::Debug + IsZero> fmt::Debug for LinkedListSparseMatrix<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_list().entries(self).finish()
    }
}

impl<'a, T: IsZero> IntoIterator for &'a LinkedListSparseMatrix<T> {
    type Item = &'a Node<T>;
    type IntoIter = Iter<'a, T>;

    fn into_iter(self) -> Self::IntoIter {
        self.iter()
    }
}

impl<'a, T: IsZero> IntoIterator for &'a mut LinkedListSparseMatrix<T> {
    type Item = &'a mut Node<T>;
    type IntoIter = IterMut<'a, T>;

    fn into_iter(self) -> Self::IntoIter {
        self.iter_mut()
    }
}

impl<'a, T: IsZero> Iterator for Iter<'a, T> {
    type Item = &'a Node<T>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.len == 0 {
            None
        } else {
            self.head.map(|node| unsafe {
                let node: &Node<T> = node.as_ref();
                self.len -= 1;
                self.head = node.next;
                node
            })
        }
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.len, Some(self.len))
    }
}

impl<T: IsZero> ExactSizeIterator for Iter<'_, T> {}

impl<'a, T: IsZero> Iterator for IterMut<'a, T> {
    type Item = &'a mut Node<T>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.len == 0 {
            None
        } else {
            self.head.map(|mut node| unsafe {
                let node: &mut Node<T> = node.as_mut();
                self.len -= 1;
                self.head = node.next;
                node
            })
        }
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.len, Some(self.len))
    }
}

impl<T: IsZero> ExactSizeIterator for IterMut<'_, T> {}

impl<T: IsZero> Node<T> {
    #[must_use]
    #[inline]
    const fn new(row: usize, column: usize, value: T) -> Self {
        Self {
            row,
            column,
            value,
            prev: None,
            next: None,
        }
    }

    #[must_use]
    #[inline]
    fn new_ptr(row: usize, column: usize, value: T) -> NonNull<Self> {
        let node = Box::new(Self::new(row, column, value));
        NonNull::from(Box::leak(node))
    }

    #[must_use]
    #[inline]
    #[allow(clippy::unnecessary_box_returns)]
    unsafe fn from_ptr(ptr: NonNull<Self>) -> Box<Self> {
        Box::from_raw(ptr.as_ptr())
    }

    #[must_use]
    #[inline]
    #[allow(clippy::boxed_local)]
    fn into_inner(self: Box<Self>) -> (usize, usize, T) {
        (self.row, self.column, self.value)
    }
}

#[allow(clippy::missing_fields_in_debug)]
impl<T: fmt::Debug + IsZero> fmt::Debug for Node<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Node")
            .field("row", &self.row)
            .field("column", &self.column)
            .field("value", &self.value)
            .finish()
}

List of Lists

稀疏矩阵的一种可能表示是列表嵌套 (List of Lists, LIL). 其中一个列表用于表示行, 每行包含三元组列表: 列索引, 值 (非零元素) 和非零元素的地址字段. 为了获得最佳性能, 两个列表都应按升序键的顺序存储.

以下面的矩阵为例:

\begin{bmatrix} \ 0 & 0 & 3 & 0 & 4 \\ 0 & 0 & 5 & 7 & 0 \\ 0 & 0 & 0 & 0 & 0 \\ 0 & 2 & 6 & 0 & 0 \ \end{bmatrix}

这个矩阵用数组存放, 效果如下图:

list of lists sparse matrix

这种存储方式的特点是:

  • Row major 风格
  • 分两层链表来存储
    • 第一层是行级链表, 存储非空行, 且以行号递增排序
    • 第二层, 在每个行链表节点中, 存储非空列的链表, 且以列号递增排序
  • 查找矩阵中某个节点的值时的性能是 O(m * n), 其中 mn 是矩阵中非 0 元素的最大行列数, 目前使用的是顺序查找, 效率比较低
  • 比较适合存放随时增减节点的矩阵, 插入或者删除元素的成本比较低, 很灵活, 但缓存不友好

算法的实现

为了简化实现, 我们使用了标准库中的双向链表实现.

比较复杂的操作是插入和删除节点, 这个要同时判断行列表和列列表都是有效的.

#![allow(unused)]
#![allow(clippy::linkedlist)]
#![allow(dead_code)]

fn main() {
use std::collections::LinkedList;
use std::fmt;

use crate::traits::IsZero;

#[derive(Debug)]
pub struct ListOfListsSparseMatrix<T: IsZero + fmt::Debug> {
    rows: LinkedList<Row<T>>,
    len: usize,
}

/// Row number in list is ordered ascending.
#[derive(Debug)]
pub struct Row<T: fmt::Debug> {
    row: usize,
    columns: LinkedList<Column<T>>,
}

/// Column number in list is ordered ascending.
#[derive(Debug)]
pub struct Column<T: fmt::Debug> {
    column: usize,
    value: T,
}

impl<T: IsZero + fmt::Debug> ListOfListsSparseMatrix<T> {
    #[must_use]
    pub fn construct<I, I2>(sparse_matrix: I) -> Self
    where
        I: IntoIterator<Item=I2>,
        I2: IntoIterator<Item=T>,
    {
        let mut row_list = LinkedList::new();
        let mut len = 0;

        for (row, rows) in sparse_matrix.into_iter().enumerate() {
            let mut column_list = LinkedList::new();
            for (column, element) in rows.into_iter().enumerate() {
                if element.is_not_zero() {
                    column_list.push_back(Column { column, value: element });
                }
            }
            if !column_list.is_empty() {
                len += column_list.len();
                row_list.push_back(Row { row, columns: column_list });
            }
        }
        Self { rows: row_list, len }
    }

    #[must_use]
    #[inline]
    pub const fn len(&self) -> usize {
        self.len
    }

    #[must_use]
    #[inline]
    pub const fn is_empty(&self) -> bool {
        self.len == 0
    }

    /// Get node value at (row, column).
    #[must_use]
    pub fn value(&self, row: usize, column: usize) -> Option<T> {
        for row_list in &self.rows {
            if row_list.row == row {
                for column_element in &row_list.columns {
                    if column_element.column == column {
                        return Some(column_element.value);
                    }
                }
                break;
            }
        }
        None
    }

    /// Get mutable reference to node value at (row, column).
    #[must_use]
    pub fn value_mut(&mut self, row: usize, column: usize) -> Option<&mut T> {
        for row_list in &mut self.rows {
            if row_list.row == row {
                for column_element in &mut row_list.columns {
                    if column_element.column == column {
                        return Some(&mut column_element.value);
                    }
                }
                break;
            }
        }
        None
    }

    /// If found old node at (row, column), returns old value; otherwise returns None.
    #[allow(dead_code)]
    pub fn add_element(&self, _ow: usize, _column: usize, _value: T) -> Option<T> {
        // 1. Find the element at (row, column)
        // 2. If no columns_list found in rows, add a new one
        // 3. Add that element to selected column_list
        todo!()
        // 1. if rows list if empty, push to back
        // 2. if front
    }

    /// If found node at (row, column), returns value of that node; otherwise returns None.
    pub fn remove_element(&mut self, row: usize, column: usize) -> Option<T> {
        // 1. Find the element at (row, column)
        // 2. Remove the element in columns list
        // 3. If the columns list is empty, remove it from rows list

        let mut value = None;
        let mut row_index = 0;
        let mut remove_column_list = false;
        for row_list in &mut self.rows {
            row_index += 1;
            if row_list.row == row {
                for column_element in &mut row_list.columns {
                    if column_element.column == column {
                        value = Some(column_element.value);
                        break;
                    }
                }

                if row_list.columns.is_empty() && value.is_some() {
                    remove_column_list = true;
                }

                break;
            }
        }

        if remove_column_list {
            let mut tail = self.rows.split_off(row_index);
            // Remove that columns list.
            tail.pop_front();
            // Then merge together again.
            if !tail.is_empty() {
                self.rows.append(&mut tail);
            }
        }
        if value.is_some() {
            self.len -= 1;
        }
        value
    }
}

pub trait IsZero: Copy {
    fn is_zero(self) -> bool;
    fn is_not_zero(self) -> bool {
        !self.is_zero()
    }
}

macro_rules! impl_is_zero {
    ($($t:ty)*) => {$(
    impl IsZero for $t {
        fn is_zero(self) -> bool {
            self == 0
        }
    }
    )*}
}

impl_is_zero! { i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize }

impl IsZero for f32 {
    fn is_zero(self) -> bool {
        self == 0.0
    }
}

impl IsZero for f64 {
    fn is_zero(self) -> bool {
        self == 0.0
    }
}
}

十字链表 Orthogonal linked list

BTree

以下面的矩阵为例:

\begin{bmatrix} \ 0 & 0 & 3 & 0 & 4 \\ 0 & 0 & 5 & 7 & 0 \\ 0 & 0 & 0 & 0 & 0 \\ 0 & 2 & 6 & 0 & 0 \ \end{bmatrix}

这个矩阵用数组存放, 效果如下图:

btree sparse matrix

这种存储方式的特点是:

  • BTree 中节点是按照 key 的顺序进行存储的, 而我们选用 (row, column) 作为 key, 这样
    • 首先以行号递增排序
    • 如果行号相同, 以列号递增排序
  • 查找/插入/删除矩阵中某个节点的值时的性能是 O(log(m * n)), 其中 mn 是矩阵中非 0 元素的最大行列数
  • 比较适合存放随时增减节点的矩阵, 插入或者删除元素的成本比较低, 很灵活
  • 支持范围查找, 比如查找某一行中所有的列
  • 实现简单

算法的实现

使用 BTree 进行存储, 实现起来最简单, 因为我们要求的接口与 BTreeMap 本身的接口非常匹配, 需要额外 花费的精力很少.

#![allow(unused)]
fn main() {
use std::collections::BTreeMap;

use crate::traits::IsZero;

#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct MatrixIndex {
    row: usize,
    column: usize,
}

/// Store sparse matrix with btree.
#[derive(Debug, Default, Clone)]
pub struct BTreeSparseMatrix<T: IsZero> {
    map: BTreeMap<MatrixIndex, T>,
}

impl<T: IsZero> BTreeSparseMatrix<T> {
    #[must_use]
    pub fn construct<I, I2>(sparse_matrix: I) -> Self
    where
        I: IntoIterator<Item=I2>,
        I2: IntoIterator<Item=T>,
    {
        let mut map = BTreeMap::new();

        for (row, row_list) in sparse_matrix.into_iter().enumerate() {
            for (column, element) in row_list.into_iter().enumerate() {
                if element.is_not_zero() {
                    map.insert(MatrixIndex { row, column }, element);
                }
            }
        }
        Self { map }
    }

    #[must_use]
    #[inline]
    pub fn len(&self) -> usize {
        self.map.len()
    }

    #[must_use]
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.map.is_empty()
    }

    /// Get node value at (row, column).
    #[must_use]
    pub fn value(&self, row: usize, column: usize) -> Option<T> {
        self.map.get(&MatrixIndex { row, column }).copied()
    }

    /// Get mutable reference to node value at (row, column).
    #[must_use]
    pub fn value_mut(&mut self, row: usize, column: usize) -> Option<&mut T> {
        self.map.get_mut(&MatrixIndex { row, column })
    }

    /// If found old node at (row, column), returns old value; otherwise returns None.
    pub fn add_element(&mut self, row: usize, column: usize, value: T) -> Option<T> {
        self.map.insert(MatrixIndex { row, column }, value)
    }

    /// If found node at (row, column), returns value of that node; otherwise returns None.
    pub fn remove_element(&mut self, row: usize, column: usize) -> Option<T> {
        self.map.remove(&MatrixIndex { row, column })
    }
}
}

动态数组 Vectors

另外, 像字符串这样的数据结构, 其底层也是使用动态数组 Vec<u8> 来实现的, 但是字符串的操作函数 更加丰富, 我们放在后面的章节单独介绍.

动态数组的常用操作

随机访问元素

尾部插入元素

任意位置插入元素

删除尾部的元素

删除任意位置的元素

标准库中 Vec 的实现

位图 BitSet

BitSet 又称为 bit map, bit array, bit mask 或者 bit vector, 是一个数组结构, 里面只存储单个的比特, 每一个比特可以表示两个状态. 它是一种很简单的集合数据结构.

位图的基本操作

位图的集合操作

位图的实现

#![allow(unused)]
fn main() {
use std::ops::Index;

const BITS_PER_ELEM: usize = 8;
const TRUE: bool = true;
const FALSE: bool = false;

#[derive(Debug, Clone)]
pub struct BitSet {
    bits: Vec<u8>,
}

impl Default for BitSet {
    fn default() -> Self {
        Self::new()
    }
}

impl BitSet {
    #[must_use]
    #[inline]
    pub const fn new() -> Self {
        Self { bits: Vec::new() }
    }

    #[must_use]
    #[inline]
    pub fn with_len(len: usize) -> Self {
        let bits_len = len.div_ceil(BITS_PER_ELEM);
        Self {
            bits: vec![0; bits_len],
        }
    }

    #[must_use]
    #[inline]
    pub fn from_bytes(bytes: &[u8]) -> Self {
        Self {
            bits: bytes.to_vec(),
        }
    }

    #[must_use]
    #[inline]
    pub fn as_bytes(&self) -> &[u8] {
        &self.bits
    }

    #[must_use]
    #[inline]
    pub fn into_vec(self) -> Vec<u8> {
        self.bits
    }

    fn expand(&mut self, index: usize) {
        let bits_len = (index + 1).div_ceil(BITS_PER_ELEM);
        if self.bits.len() < bits_len {
            // TODO(Shaohua): Adjust resize policy.
            self.bits.resize(bits_len, 0);
        }
    }

    pub fn set(&mut self, index: usize) {
        self.expand(index);
        let word = index / BITS_PER_ELEM;
        let bit = index % BITS_PER_ELEM;
        let flag = 1 << bit;
        self.bits[word] |= flag;
    }

    pub fn unset(&mut self, index: usize) {
        self.expand(index);
        let word = index / BITS_PER_ELEM;
        let bit = index % BITS_PER_ELEM;
        let flag = 1 << bit;
        self.bits[word] &= !flag;
    }

    pub fn flip(&mut self, index: usize) {
        self.expand(index);
        let word = index / BITS_PER_ELEM;
        let bit = index % BITS_PER_ELEM;
        let flag = 1 << bit;
        // FIXME(Shaohua):
        self.bits[word] &= !flag;
    }

    /// Check bit at `index` is set or not.
    #[must_use]
    pub fn get(&self, index: usize) -> Option<bool> {
        let word = index / BITS_PER_ELEM;
        if word >= self.bits.len() {
            return None;
        }
        let bit = index % BITS_PER_ELEM;
        let flag = 1 << bit;
        Some((self.bits[word] & flag) == flag)
    }

    /// Returns the number of bits set to `true`.
    #[must_use]
    pub fn count_ones(&self) -> usize {
        self.bits
            .iter()
            .map(|byte| byte.count_ones() as usize)
            .sum()
    }

    /// Returns the number of bits set to `false`.
    #[must_use]
    pub fn count_zeros(&self) -> usize {
        self.bits
            .iter()
            .map(|byte| byte.count_zeros() as usize)
            .sum()
    }

    #[must_use]
    #[inline]
    pub const fn iter(&self) -> BitSetIter {
        BitSetIter {
            bit_set: self,
            index: 0,
        }
    }

    /// # Panics
    /// Raise panic if length of two bitset not equal.
    #[must_use]
    pub fn union(&self, other: &Self) -> Self {
        assert_eq!(self.bits.len(), other.bits.len());
        let bits = self
            .bits
            .iter()
            .zip(other.bits.iter())
            .map(|(a, b)| a | b)
            .collect();
        Self { bits }
    }

    /// # Panics
    /// Raise panic if length of two bitset not equal.
    #[must_use]
    pub fn intersect(&self, other: &Self) -> Self {
        assert_eq!(self.bits.len(), other.bits.len());
        let bits = self
            .bits
            .iter()
            .zip(other.bits.iter())
            .map(|(a, b)| a & b)
            .collect();
        Self { bits }
    }

    /// # Panics
    /// Raise panic if length of two bitset not equal.
    #[must_use]
    pub fn difference(&self, other: &Self) -> Self {
        assert_eq!(self.bits.len(), other.bits.len());
        let bits = self
            .bits
            .iter()
            .zip(other.bits.iter())
            .map(|(a, b)| a & !b)
            .collect();
        Self { bits }
    }
}

impl From<String> for BitSet {
    fn from(value: String) -> Self {
        Self {
            bits: value.into_bytes(),
        }
    }
}

impl From<&str> for BitSet {
    fn from(s: &str) -> Self {
        Self::from_bytes(s.as_bytes())
    }
}

macro_rules! from_number_impl {
    ($($t:ty)*) => {$(
        impl From<$t> for BitSet {
            fn from(value: $t) -> Self {
                Self {
                    bits: value.to_le_bytes().to_vec(),
                }
            }
        }
    )*};
}

from_number_impl! {i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize}

impl Index<usize> for BitSet {
    type Output = bool;

    fn index(&self, index: usize) -> &Self::Output {
        if self.get(index).expect("index out of range") {
            &TRUE
        } else {
            &FALSE
        }
    }
}

pub struct BitSetIter<'a> {
    bit_set: &'a BitSet,
    index: usize,
}

impl Iterator for BitSetIter<'_> {
    type Item = bool;

    fn next(&mut self) -> Option<Self::Item> {
        let is_set = self.bit_set.get(self.index);
        if is_set.is_some() {
            self.index += 1;
        }
        is_set
    }
}

impl<'a> IntoIterator for &'a BitSet {
    type IntoIter = BitSetIter<'a>;
    type Item = bool;
    fn into_iter(self) -> Self::IntoIter {
        self.iter()
    }
}
}

位图的应用

记录每天的活跃用户数量

每天有多少个独立用户访问了服务? 在后端的数据库里, 用户的ID通常都是自增的. 我们可以基于每天的访问日志, 提供出用户的ID, 然后存放一个 BitSet 中, 它会自动去重, 并且只占用很少的空间.

对于有 1000万 用户的应用, 也只需要 10M / 8 = 1.25M 的空间来存储它. 将 bitset 对象存储到硬盘上后, 还可以使用 zstd/gzip 等工具对它进行压缩, 可以进一步降低占用的空间.

另外, 还可以对不同日期的 biset 进行逻辑与 (AND) 操作, 可以提取出极度活跃的用户.

对整数数组快速排序并去重

如果数组中都是整数, 而且数字间比较连续, 比较稠密, 数值的范围也是确定的, 并且要移除重复的元素, 那就可以考虑使用位图来进行快速排序和去重.

使用比特位的下标作为整数值, 这样的话只需要一个比特位就可以表示一个整数, 与 Vec<i32> 等存储结构相比, 位图可以显著地节省存储空间.

use std::fs::File;
use std::io::{self, Read};

use vector::bitset::BitSet;

pub fn random_ints(len: usize) -> Result<Vec<u32>, io::Error> {
    let mut file = File::open("/dev/urandom")?;
    let mut buf = vec![0; len * 4];
    file.read_exact(&mut buf)?;

    let mut nums = vec![0; len];
    for i in 0..len {
        let array: [u8; 4] = [buf[4 * i], buf[4 * i + 1], buf[4 * i + 2], buf[4 * i + 3]];
        let num = u32::from_le_bytes(array);
        nums[i] = num.min(10_000_000);
    }
    Ok(nums)
}

fn main() {
    let mut numbers = random_ints(10_000_000).unwrap();
    let max_number = numbers.iter().max().copied().unwrap_or_default() as usize;
    let mut bit_set = BitSet::with_len(max_number);
    for &num in &numbers {
        bit_set.set(num as usize);
    }

    let sorted: Vec<u32> = bit_set
        .iter()
        .enumerate()
        .filter(|(_index, is_set)| *is_set)
        .map(|(index, _is_set)| index as u32)
        .collect();

    numbers.sort_unstable();
    numbers.dedup();
    assert_eq!(numbers, sorted);
}

时间复杂度是 O(n), 空间复杂度是 O(n).

参考

布隆过滤器 Bloom filter

参考

Hashed Array Tree

参考

字符串 String

字符串编码

字符串的常用操作

链表 List

数组 类似, 链表也是计算机科学里的常用的数据结构.

与数组相比, 链表的最大特点是:

  • 支持高效地在任意节点位置插入和删除元素
  • 在内存中非连续地存储各个元素
  • 不支持随机访问各个元素

链表是线性数据结构 (linear data structure), 它由一系列的节点组成; 节点内部保存着元素的值, 节点之间使用指针或者引用引连, 可以顺着指针/引用找到下个节点在内存中的位置.

list layout

链表的类型

根据链表的结构, 有这几种类型:

  • 单链表 singly linked list
  • 双链表 doubly linked list
  • 环状链表 circular linked list
  • 环状双链表 doubly circular linked list
  • header linked list
  • multiply linked list
  • unrolled linked list

单链表 Singly Linked List

在单链表中, 每个节点包括一个指针, 指向下个节点.

特点:

  • 只能从链表头部单向地遍历整个链表
  • 每个节点只需要存储一个指针元素, 可以节省一些内存空间

单链表的结构如下图所示:

singly linked list

C语言中对应的结构体声明如下:

// 单链表
struct singly_list_s {
  int value;
  struct singly_list_s* next;
};

双链表 Doubly Linked List

在双链表中, 每个节点持有两个指针, 分别指向它的前一个节点以及后一个节点.

特点:

  • 可以向前和向后双向遍历整个链表
  • 每个节点要存储两个指针, 占用更多的内存空间

双链表的结构如下图所示:

doubly linked list

C语言中对应的结构体声明如下:

// 双链表
struct doubly_list_s {
  int value;
  struct doubly_list_s* previous;
  struct doubly_list_s* next;
};

环状链表 Circular Linked List

与单链表不同, 环状链表的最后一个节点指向链表的第一个节点, 形成一个环.

特点是:

  • 遍历环状链表一周后, 可以回到起始节点

环状链表的结构如下图所示:

circular linked list

C语言中对应的结构体声明如下:

// 环状链表
struct circular_list_s {
  int value;
  struct circular_list_s* next;
};

环状双链表 Doubly Circular Linked List

与双链表不同, 环状双链表的首尾节点也有指针相互链表, 所以它里面不存在指向空节点的指针.

特点:

  • 支持向前向后双向遍历
  • 遍历链表一周之后会回到起始点

环状双链表的结构如下图所示:

doubly circular linked list

C语言中对应的结构体声明如下:

// 环状双链表
struct doubly_circular_list_s {
  int value;
  struct doubly_circular_list_s* previous;
  struct doubly_circular_list_s* next;
};

Header Linked List

这种链表是对单链表的改进, 在实现的编码中, 如果链表指针指向链表中的第一个节点时, 有很多操作, 比如删除节点或者交换节点的操作, 处理起来比较麻烦, 需要单独考虑第一个节点.

为此, 我们可以在第一个节点之前再加一个 header node, 或者称为 dummy node, 链表的指针 指向该节点, 然后该节点再指向链表的真正存放数据元素的第一个节点.

特点:

  • 支持向后单向遍历节点
  • 更方便针对链表节点的操作

该链表的结构如下图所示:

header linked list

C语言中对应的结构体声明如下:

// 环状双链表
struct doubly_circular_list_s {
  int value;
  struct doubly_circular_list_s* previous;
  struct doubly_circular_list_s* next;
};

Multiply Linked List

上面介绍的双链表, 每个节点有两个指针分别指向节点的前后相邻节点.

如果一个节点中有多个指针指向别的节点呢? 这就是 Multiply Linked List, 或者称为 Multi-level Linked List.

特点:

  • 节点之间有多个连接
  • 遍历节点的方式有多种

C语言中的结构体声明如下:

// Multiply Linked List
struct multi_list_s {
    int value;
    int len;
    struct multi_list_s* right;
    struct multi_list_s* bottom;

该类链表可以表示基于不同方式排序节点, 比如用于记录个人信息:

struct person {
  char* name;
  int age;
};

记录个人信息的列表, 可以基于人的姓名排序, 也可以基于年龄排序, 其结构图如下所示:

person linked list

或者展示稀疏矩阵:

-012
0050
1000
220010
3600

使用以下数据结构:

struct coordinate_s {
  int row;
  int column;
};

struct sparse_matrix_s {
  struct coordinate_s coord;
  int value;
  struct sparse_matrix_s* next_row;
  struct sparse_matrix_s* next_column;
};

这样的稀疏矩阵可以同时基于行号和列号进行线性查找, 比较方便. 其结构图如下所示:

sparse matrix

或者表示多层链表 Multi-level linked list, 又称十字链表法 Orthogonal linked list.如下图所示:

multi-level linked list

还有一种简化了的, 称为 List of lists (LIL), 这种的, 索引方式要简单些. 如下图所示:

list of lists

Unrolled Linked List

上面的链表中, 每个节点都只存储一个元素值, 我们也可以在一个节点中存储多个元素值. 这种链表就是 Unrolled Linked List.

特点:

  • CPU 缓存更友好, 提高缓存命中率
  • 访问相邻的元素的性能更好

其结构图如下所示:

unrolled linked list

C语言中的结构体声明如下:


// Unrolled Linked List
struct unrolled_list_s {
  struct unrolled_list_s* next;
  int len;
  int elements[0];

链表的基本操作

常用的链表操作比较多.

构造函数:

  • new(), 创建一个新的链表, 不包含任何节点

元素访问:

  • front(), 返回第一个元素的引用
  • front_mut(), 返回第一个元素的可变更引用
  • back(), 返回最后一个元素的引用
  • back_mut(), 返回最后一个元素的可变更引用
  • contains(value), 检查链表中是否包含给定的元素

链表容量:

  • len(), 返回节点个数
  • is_empty(), 链表是否为空

修改链表:

  • clear(), 移除链表中的所有节点, 移除之后, len() 函数返回 0
  • insert_at(pos, value), 在给定的特定位置插入新的节点
  • insert_iter(pos, iter), 在给定的特定位置插入一系列的节点
  • pop(), 从链表中移除特定值相等的第一个节点
  • pop_at(pos), 在给定的特定位置移除节点, 并返回该节点的值
  • pop_if(), 从链表中移除满足特定条件的所有节点
  • push_back(), 在链表尾部追加新的节点
  • pop_back(), 移除链表尾部的节点
  • push_front(), 在链表头部加入新的节点
  • pop_front(), 移除链表头部的节点
  • resize(new_size), 调整链表中节点的个数, 如果需要追加新的节点, 就使用默认值
  • resize_with(new_size, new_value), 调整链表中节点的个数, 如果需要追加新的节点, 就使用 new_value
  • append(list), 在链表尾部追加一系列的节点
  • prepend(list), 在链表头部加入一系列的节点

链表操作:

  • merge(), 合并两个链表
  • splice(), 将节点从一个链表转移到另一个链表
  • reverse(), 将链表中的节点反转
  • unique(), 从链表中移除有相同值的相邻的节点
  • sort(), 对链表中的节点进行排序, 排序相关的函数放在了后面排序算法章节
  • sort_by(), 依照相应的条件函数对链表中的节点进行排序
  • sort_by_key(), 依照相应的条件对链表中的节点进行排序

实现的 traits:

  • Debug
  • Clone
  • PartialEq
  • Eq
  • Hash
  • Drop
  • FromIterator
  • Extend

迭代器:

  • iter(), 返回一个迭代器
  • iter_mut(), 返回一个迭代器, 可以通过它修改链表中节点的值
  • into_iter()
  • DoubleEndedIterator, 对于双链表, 返回的迭代器需要实现双向迭代

插入 Insertion

在链表中插入一个新的节点, 分好几种情况:

  • 在链表的头部插入节点
  • 在链表的尾部插入节点
  • 在给定的索引位置插入节点
  • 在给定的节点后面插入节点

单链表 Singly Linked List

双链表 Doubly Linked List

相对于前文提到的单链表, 双链表 doubly linked list (DLL) 中每个节点包含两个指针, 分别指向左右相邻的节点.

其结构如下图所示:

doubly linked list

双链表的优点:

  • 反转双向链表非常容易
  • 它可以在执行过程中轻松分配或重新分配内存
  • 与单链表一样, 它是最容易实现的数据结构
  • 此双向链表的遍历是双向的, 这在单链表中是不可能的
  • 与单链表相比, 删除节点很容易. 单链表删除需要指向要删除的节点和前一个节点的指针, 但在双向链表中, 它只需要要删除的指针. 与其他数据结构 (如数组) 相比, 双向链表的开销较低
  • 可用于实现图算法 graph algorithms

双链表的不足:

  • 与数组和单链表相比, 它使用额外的内存来存储左侧相邻接点
  • 由于内存中的元素是随机存储的, 因此元素是按顺序访问的, 不允许随机访问
  • 遍历双向链表可能比遍历单链表慢
  • 实现和维护双向链表可能比单链表更复杂

双链表的应用场景:

  • 它用于需要前后双向访问的系统
  • 浏览器使用它来实现访问过的网页的前后导航, 即后退和前进按钮
  • 它也用于表示经典的纸牌游戏
  • 各种应用程序也使用它来实现撤消和重做功能
  • 双向链表也用于构建 MRU/LRU (最近使用/最近最少使用) 缓存系统
  • 其他数据结构, 如堆栈, 哈希表, 二叉树也可以使用双向链表构建或编程
  • 在许多操作系统中, 线程调度程序 scheduler (选择哪个进程需要在什么时候运行的东西) 维护当时运行的所有进程的双向链表
  • 实现图算法

链表的节点信息

我们使用单独的链表表头来记录链表的头尾节点信息, 同时还单独记录链表中节点的个数 len.

每个节点包含两个指针分别指向左右相邻节点, 同时在节点上存储元数的值.

其结构大致如下:

#![allow(unused)]

fn main() {
pub struct DoublyLinkedList<T> {
    head: NodePtr<T>,
    tail: NodePtr<T>,
    len: usize,
    _marker: PhantomData<Box<Node<T>>>,
}

type NodePtr<T> = Option<NonNull<Node<T>>>;

struct Node<T> {
    prev: NodePtr<T>,
    next: NodePtr<T>,
    value: T,
}
}

插入节点

批量插入节点

移除节点

访问节点

只有头部节点和尾部节点可以直接访问, 其它节点需要遍历之后才能访问.

#![allow(unused)]
fn main() {
    /// Access the first node.
    #[must_use]
    #[inline]
    pub fn front(&self) -> Option<&T> {
        unsafe { self.head.as_ref().map(|node| &node.as_ref().value) }
    }

    /// Access the first node exclusively.
    #[must_use]
    #[inline]
    pub fn front_mut(&mut self) -> Option<&mut T> {
        unsafe { self.head.as_mut().map(|node| &mut node.as_mut().value) }
    }

    /// Access the last node.
    #[must_use]
    #[inline]
    pub fn back(&self) -> Option<&T> {
        unsafe { self.tail.as_ref().map(|node| &node.as_ref().value) }
    }

    /// Access the last node exclusively.
    #[must_use]
    #[inline]
    pub fn back_mut(&mut self) -> Option<&mut T> {
        unsafe { self.tail.as_mut().map(|node| &mut node.as_mut().value) }
    }
}

迭代器

反转链表

链表分隔

合并链表

算法实现

#![allow(unused)]
fn main() {
use std::cmp::Ordering;
use std::fmt::Formatter;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::ptr::NonNull;
use std::{fmt, mem};

pub struct DoublyLinkedList<T> {
    head: NodePtr<T>,
    tail: NodePtr<T>,
    len: usize,
    _marker: PhantomData<Box<Node<T>>>,
}

type NodePtr<T> = Option<NonNull<Node<T>>>;

struct Node<T> {
    prev: NodePtr<T>,
    next: NodePtr<T>,
    value: T,
}

pub struct IntoIter<T>(DoublyLinkedList<T>);

pub struct Iter<'a, T: 'a> {
    head: NodePtr<T>,
    tail: NodePtr<T>,
    len: usize,
    _marker: PhantomData<&'a Node<T>>,
}

pub struct IterMut<'a, T: 'a> {
    head: NodePtr<T>,
    tail: NodePtr<T>,
    len: usize,
    _marker: PhantomData<&'a mut Node<T>>,
}

// Public functions for list.
impl<T> DoublyLinkedList<T> {
    /// Create an empty list.
    #[must_use]
    #[inline]
    pub const fn new() -> Self {
        Self {
            len: 0,
            head: None,
            tail: None,
            _marker: PhantomData,
        }
    }

    // Element access

    /// Access the first node.
    #[must_use]
    #[inline]
    pub fn front(&self) -> Option<&T> {
        unsafe { self.head.as_ref().map(|node| &node.as_ref().value) }
    }

    /// Access the first node exclusively.
    #[must_use]
    #[inline]
    pub fn front_mut(&mut self) -> Option<&mut T> {
        unsafe { self.head.as_mut().map(|node| &mut node.as_mut().value) }
    }

    /// Access the last node.
    #[must_use]
    #[inline]
    pub fn back(&self) -> Option<&T> {
        unsafe { self.tail.as_ref().map(|node| &node.as_ref().value) }
    }

    /// Access the last node exclusively.
    #[must_use]
    #[inline]
    pub fn back_mut(&mut self) -> Option<&mut T> {
        unsafe { self.tail.as_mut().map(|node| &mut node.as_mut().value) }
    }

    pub fn contains(&self, value: &T) -> bool
    where
        T: PartialEq<T>,
    {
        self.iter().any(|item| item == value)
    }

    // Capacity operations

    /// Returns the number of elements in list.
    #[must_use]
    #[inline]
    pub const fn len(&self) -> usize {
        self.len
    }

    /// Check whether the list is empty.
    #[must_use]
    #[inline]
    pub const fn is_empty(&self) -> bool {
        self.len == 0
    }

    // Iterators
    pub fn iter(&self) -> Iter<'_, T> {
        Iter {
            head: self.head,
            tail: self.tail,
            len: self.len,
            _marker: Default::default(),
        }
    }

    pub fn iter_mut(&mut self) -> IterMut<'_, T> {
        IterMut {
            head: self.head,
            tail: self.tail,
            len: self.len,
            _marker: Default::default(),
        }
    }

    // Modifiers

    /// Clear the contents.
    ///
    /// Erases all elements from the list.
    /// After calling this function, size of list is zero.
    pub fn clear(&mut self) {
        let mut other = Self::new();
        mem::swap(self, &mut other);
        drop(other);
    }

    /// Insert element at `pos`.
    ///
    /// # Panics
    ///
    /// Panic if `index > len`.
    pub fn insert_at(&mut self, mut pos: usize, value: T) {
        assert!(pos <= self.len);
        if pos == 0 {
            self.push_front(value);
            return;
        }
        if pos == self.len {
            self.push_back(value);
            return;
        }

        let new_node_ptr = Node::new_ptr(value);
        if let Some(mut node) = self.head {
            while let Some(next_node) = unsafe { node.as_mut().next } {
                if pos == 1 {
                    break;
                }
                pos -= 1;
                node = next_node;
            }

            unsafe {
                Self::insert_after(node, new_node_ptr);
            }
            self.len += 1;
        }
    }

    pub fn insert_iter<I: IntoIterator<Item = T>>(&mut self, mut pos: usize, iter: I) {
        assert!(pos <= self.len);
        let mut new_list = DoublyLinkedList::from_iter(iter);

        if pos == 0 {
            self.prepend(&mut new_list);
            return;
        }
        if pos == self.len {
            self.append(&mut new_list);
            return;
        }

        if let Some(mut node) = self.head {
            while let Some(next_node) = unsafe { node.as_mut().next } {
                if pos == 1 {
                    break;
                }
                pos -= 1;
                node = next_node;
            }

            self.len += new_list.len();
            unsafe {
                Self::append_nodes(node, &mut new_list);
            }
        }
    }

    /// Removes the first element equals specific `value`.
    pub fn pop(&mut self, value: &T) -> Option<T>
    where
        T: PartialEq<T>,
    {
        for (index, item) in self.iter().enumerate() {
            if item == value {
                return self.pop_at(index);
            }
        }
        None
    }

    /// Removes the first element satisfying specific condition and returns that element.
    pub fn pop_if<F>(&mut self, f: F) -> Option<T>
    where
        F: Fn(&T) -> bool,
    {
        let mut index: usize = 0;
        for item in self.iter() {
            if f(item) {
                break;
            }
            index += 1;
        }
        self.pop_at(index)
    }

    /// Remove element at `pos` and returns that element.
    ///
    /// # Panics
    ///
    /// Raise panic if `pos >= len`
    pub fn pop_at(&mut self, mut pos: usize) -> Option<T> {
        assert!(pos < self.len);
        if pos == 0 {
            return self.pop_front();
        }
        if pos == self.len - 1 {
            return self.pop_back();
        }
        if let Some(mut node) = self.head {
            while let Some(next_node) = unsafe { node.as_mut().next } {
                if pos == 1 {
                    break;
                }
                pos -= 1;
                node = next_node;
            }

            self.len -= 1;
            unsafe { Self::remove_after(node).map(Node::into_inner) }
        } else {
            None
        }
    }

    /// Add an element to the beginning of list.
    pub fn push_front(&mut self, value: T) {
        let node_ptr = Node::new_ptr(value);
        self.push_front_node(node_ptr);
    }

    /// Remove the first node in the list.
    pub fn pop_front(&mut self) -> Option<T> {
        self.pop_front_node().map(Node::into_inner)
    }

    /// Add an element to the end of list.
    pub fn push_back(&mut self, value: T) {
        let node_ptr = Node::new_ptr(value);
        self.push_back_node(node_ptr);
    }

    /// Remove the last node in the list.
    pub fn pop_back(&mut self) -> Option<T> {
        self.pop_back_node().map(Node::into_inner)
    }

    /// Append all elements in another list to self.
    pub fn append(&mut self, other: &mut Self) {
        match self.tail {
            Some(mut tail) => {
                // connect tail of self to head of other.
                if let Some(mut other_head) = other.head.take() {
                    unsafe {
                        tail.as_mut().next = Some(other_head);
                        other_head.as_mut().prev = Some(tail);
                    }

                    self.tail = other.tail.take();
                    self.len += other.len();
                    other.len = 0;
                }
            }
            None => {
                // self is empty.
                mem::swap(self, other);
            }
        }
    }

    /// Prepend all elements in another list to self.
    #[inline]
    pub fn prepend(&mut self, other: &mut Self) {
        other.append(self);
        self.swap(other);
    }

    /// Change number of elements stored.
    ///
    /// If the current size is greater than `new_size`, extra elements will
    /// be removed.
    /// If current size is less than `new_size`, more elements with default
    /// value are appended.
    pub fn resize(&mut self, new_size: usize)
    where
        T: Default,
    {
        match self.len.cmp(&new_size) {
            Ordering::Equal => (),
            Ordering::Less => {
                for _ in 0..(new_size - self.len) {
                    self.push_back(T::default());
                }
            }
            Ordering::Greater => {
                for _ in 0..(self.len - new_size) {
                    let _node = self.pop_back_node();
                }
            }
        }
    }

    /// Change number of elements stored.
    pub fn resize_with(&mut self, new_size: usize, value: T)
    where
        T: Clone,
    {
        match self.len.cmp(&new_size) {
            Ordering::Equal => (),
            Ordering::Less => {
                for _ in 0..(new_size - self.len) {
                    self.push_back(value.clone());
                }
            }
            Ordering::Greater => {
                for _ in 0..(self.len - new_size) {
                    let _node = self.pop_back_node();
                }
            }
        }
    }

    /// Swap the contents.
    #[inline]
    pub fn swap(&mut self, other: &mut Self) {
        mem::swap(self, other);
    }

    // Operations

    /// Merges two sorted lists.
    pub fn merge(&mut self, _other: &mut Self)
    where
        T: PartialOrd<T>,
    {
        todo!()
    }

    // pub fn merge_by(&mut self, other: &mut Self, predict: P)
    // where
    //     P: PartialOrd<T>,
    // {
    //     todo!()
    // }

    /// Move elements from another list.
    pub fn splice(&mut self, _other: &mut Self) {
        todo!()
    }

    /// Reverses the order of the elements.
    pub fn reverse(&mut self) {
        unsafe { Self::base_reverse(self.head) };
        mem::swap(&mut self.head, &mut self.tail);
    }

    /// Removes consecutive duplicate elements.
    ///
    /// Returns number of elements removed.
    pub fn unique(&mut self) -> usize
    where
        T: PartialEq<T>,
    {
        let mut count = 0;
        if let Some(mut node) = self.head {
            while let Some(next_node) = unsafe { node.as_mut().next } {
                unsafe {
                    if node.as_ref().value == next_node.as_ref().value {
                        Self::remove_after(node);
                        count += 1;
                    } else {
                        node = next_node;
                    }
                }
            }
        }

        count
    }

    pub fn sort(&mut self) {
        todo!()
    }

    //pub fn sort_by(&mut self) { }
    //pub fn sort_by_key(&mut self) { }
}

// Private or unsafe functions for list.
impl<T> DoublyLinkedList<T> {
    fn push_front_node(&mut self, node_ptr: NonNull<Node<T>>) {
        unsafe {
            (*node_ptr.as_ptr()).next = self.head;
            (*node_ptr.as_ptr()).prev = None;
        }
        let node = Some(node_ptr);

        match self.head {
            Some(head) => unsafe { (*head.as_ptr()).prev = node },
            None => self.tail = node,
        }

        self.head = node;
        self.len += 1;
    }

    fn push_back_node(&mut self, node_ptr: NonNull<Node<T>>) {
        unsafe {
            (*node_ptr.as_ptr()).next = None;
            (*node_ptr.as_ptr()).prev = self.tail;
        }
        let node = Some(node_ptr);

        match self.tail {
            Some(tail) => unsafe { (*tail.as_ptr()).next = node },
            None => self.head = node,
        }

        self.tail = node;
        self.len += 1;
    }

    fn pop_front_node(&mut self) -> Option<Box<Node<T>>> {
        self.head.map(|old_head| {
            let old_head = unsafe { Node::from_ptr(old_head) };
            self.head = old_head.next;

            match self.head {
                Some(head) => unsafe { (*head.as_ptr()).prev = None },
                None => self.tail = None,
            }

            self.len -= 1;
            old_head
        })
    }

    fn pop_back_node(&mut self) -> Option<Box<Node<T>>> {
        self.tail.map(|old_tail| {
            let old_tail = unsafe { Node::from_ptr(old_tail) };
            self.tail = old_tail.prev;

            match self.tail {
                Some(tail) => unsafe { (*tail.as_ptr()).next = None },
                None => self.head = None,
            }

            self.len -= 1;
            old_tail
        })
    }

    unsafe fn insert_after(mut prev_node: NonNull<Node<T>>, mut new_node_ptr: NonNull<Node<T>>) {
        if let Some(mut next_node) = prev_node.as_mut().next {
            new_node_ptr.as_mut().next = Some(next_node);
            next_node.as_mut().prev = Some(new_node_ptr);
        }
        new_node_ptr.as_mut().prev = Some(prev_node);
        prev_node.as_mut().next = Some(new_node_ptr);
    }

    unsafe fn append_nodes(mut prev_node: NonNull<Node<T>>, other: &mut Self) {
        if other.is_empty() {
            return;
        }

        if let Some(mut next_node) = prev_node.as_mut().next {
            if let Some(mut other_tail) = other.tail.take() {
                other_tail.as_mut().next = Some(next_node);
                next_node.as_mut().prev = Some(other_tail);
            }
        }
        if let Some(mut other_head) = other.head.take() {
            prev_node.as_mut().next = Some(other_head);
            other_head.as_mut().prev = Some(prev_node);
        }

        other.len = 0;
    }

    unsafe fn remove_after(mut node: NonNull<Node<T>>) -> Option<Box<Node<T>>> {
        if let Some(mut next_node) = node.as_mut().next {
            let mut next_next_node = next_node.as_mut().next.take();
            if let Some(next_next_node) = next_next_node.as_mut() {
                next_next_node.as_mut().prev = Some(node);
            }
            node.as_mut().next = next_next_node;

            Some(Node::from_ptr(next_node))
        } else {
            None
        }
    }

    unsafe fn base_reverse(node: NodePtr<T>) {
        let mut temp = node;
        while let Some(mut temp_node) = temp {
            mem::swap(&mut temp_node.as_mut().prev, &mut temp_node.as_mut().next);
            // Old next node is now prev.
            temp = temp_node.as_mut().prev;
        }
    }
}

impl<T> Drop for DoublyLinkedList<T> {
    fn drop(&mut self) {
        while self.pop_front_node().is_some() {
            // dropped
        }
    }
}

impl<T> Default for DoublyLinkedList<T> {
    #[inline]
    fn default() -> Self {
        Self::new()
    }
}

impl<T: fmt::Debug> fmt::Debug for DoublyLinkedList<T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_list().entries(self).finish()
    }
}

impl<T: Clone> Clone for DoublyLinkedList<T> {
    fn clone(&self) -> Self {
        let mut list = Self::new();
        list.extend(self.iter().cloned());
        list
    }
}

impl<T: PartialEq> PartialEq for DoublyLinkedList<T> {
    fn eq(&self, other: &Self) -> bool {
        self.len == other.len && self.iter().eq(other.iter())
    }
}

impl<T: Eq> Eq for DoublyLinkedList<T> {}

impl<T: Hash> Hash for DoublyLinkedList<T> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        // state.write_length_prefix(self.len);
        state.write_usize(self.len);
        for element in self {
            element.hash(state);
        }
    }
}

impl<T> Extend<T> for DoublyLinkedList<T> {
    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
        iter.into_iter().for_each(|value| self.push_back(value));
    }
}

impl<T> FromIterator<T> for DoublyLinkedList<T> {
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
        let mut list = Self::new();
        list.extend(iter);
        list
    }
}

impl<T> IntoIterator for DoublyLinkedList<T> {
    type Item = T;
    type IntoIter = IntoIter<T>;

    fn into_iter(self) -> Self::IntoIter {
        IntoIter(self)
    }
}

impl<'a, T> IntoIterator for &'a DoublyLinkedList<T> {
    type Item = &'a T;
    type IntoIter = Iter<'a, T>;

    fn into_iter(self) -> Self::IntoIter {
        self.iter()
    }
}

impl<'a, T> IntoIterator for &'a mut DoublyLinkedList<T> {
    type Item = &'a mut T;
    type IntoIter = IterMut<'a, T>;

    fn into_iter(self) -> Self::IntoIter {
        self.iter_mut()
    }
}

impl<T> Iterator for IntoIter<T> {
    type Item = T;

    #[inline]
    fn next(&mut self) -> Option<T> {
        self.0.pop_front()
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.0.len, Some(self.0.len))
    }
}

impl<T> DoubleEndedIterator for IntoIter<T> {
    #[inline]
    fn next_back(&mut self) -> Option<Self::Item> {
        self.0.pop_back()
    }
}

impl<T> ExactSizeIterator for IntoIter<T> {}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        if self.len == 0 {
            None
        } else {
            self.head.map(|node| unsafe {
                let node: &Node<T> = node.as_ref();
                self.len -= 1;
                self.head = node.next;
                &node.value
            })
        }
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.len, Some(self.len))
    }

    #[inline]
    fn last(mut self) -> Option<Self::Item>
    where
        Self: Sized,
    {
        self.next_back()
    }
}

impl<T> DoubleEndedIterator for Iter<'_, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.len == 0 {
            None
        } else {
            self.tail.map(|node| unsafe {
                let node: &Node<T> = node.as_ref();
                self.tail = node.prev;
                self.len -= 1;
                &node.value
            })
        }
    }
}

impl<T> ExactSizeIterator for Iter<'_, T> {}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;

    fn next(&mut self) -> Option<Self::Item> {
        if self.len == 0 {
            None
        } else {
            self.head.map(|mut node| unsafe {
                let node: &mut Node<T> = node.as_mut();
                self.len -= 1;
                self.head = node.next;
                &mut node.value
            })
        }
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.len, Some(self.len))
    }

    #[inline]
    fn last(mut self) -> Option<Self::Item>
    where
        Self: Sized,
    {
        self.next_back()
    }
}

impl<T> DoubleEndedIterator for IterMut<'_, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.len == 0 {
            None
        } else {
            self.tail.map(|mut node| unsafe {
                let node: &mut Node<T> = node.as_mut();
                self.tail = node.prev;
                self.len -= 1;
                &mut node.value
            })
        }
    }
}

impl<T> ExactSizeIterator for IterMut<'_, T> {}

impl<T> Node<T> {
    #[must_use]
    #[inline]
    const fn new(value: T) -> Self {
        Self {
            prev: None,
            next: None,
            value,
        }
    }

    #[must_use]
    #[inline]
    fn new_ptr(value: T) -> NonNull<Self> {
        let node = Box::new(Self::new(value));
        NonNull::from(Box::leak(node))
    }

    #[must_use]
    #[inline]
    unsafe fn from_ptr(ptr: NonNull<Self>) -> Box<Self> {
        Box::from_raw(ptr.as_ptr())
    }

    #[must_use]
    #[inline]
    #[allow(clippy::boxed_local)]
    fn into_inner(self: Box<Self>) -> T {
        self.value
    }
}

#[cfg(test)]
mod tests {
    use super::DoublyLinkedList;

    #[test]
    fn test_is_empty() {
        let list = DoublyLinkedList::<i32>::new();
        assert!(list.is_empty());
    }

    #[test]
    fn test_push() {
        let mut list = DoublyLinkedList::new();
        list.push_front(2);
        list.push_front(3);
        list.push_front(5);
        list.push_front(7);
        list.push_front(11);
        assert_eq!(list.len(), 5);
    }

    #[test]
    fn test_pop_front() {
        let mut list = DoublyLinkedList::new();
        list.push_front(3);
        list.push_front(5);
        list.push_front(7);
        assert_eq!(list.pop_front(), Some(7));
        assert_eq!(list.len(), 2);
        assert_eq!(list.pop_front(), Some(5));
        assert_eq!(list.pop_front(), Some(3));
        assert!(list.is_empty());
    }

    #[test]
    fn test_pop_back() {
        let mut list = DoublyLinkedList::new();
        list.push_back(3);
        list.push_back(5);
        list.push_back(7);
        assert_eq!(list.pop_back(), Some(7));
        assert_eq!(list.len(), 2);
        assert_eq!(list.pop_back(), Some(5));
        assert_eq!(list.pop_back(), Some(3));
        assert!(list.is_empty());
    }

    #[test]
    fn test_back() {
        let mut list = DoublyLinkedList::new();
        list.push_back(5);
        list.push_back(7);
        assert_eq!(list.back(), Some(&7));
        assert_eq!(list.front(), Some(&5));
    }

    #[test]
    fn test_back_mut() {
        let mut list = DoublyLinkedList::new();
        list.push_back(5);
        list.push_back(7);
        if let Some(value) = list.back_mut() {
            *value = 11;
        }
        assert_eq!(list.back(), Some(&11));
    }

    #[test]
    fn test_drop() {
        let mut list = DoublyLinkedList::new();
        for i in 0..(128 * 200) {
            list.push_front(i);
        }
        drop(list);
    }

    #[test]
    fn test_into_iter() {
        let mut list = DoublyLinkedList::new();
        list.push_front(2);
        list.push_front(3);
        list.push_front(5);
        list.push_front(7);
        let mut iter = list.into_iter();
        assert_eq!(iter.next(), Some(7));
        assert_eq!(iter.next(), Some(5));
        assert_eq!(iter.next(), Some(3));
        assert_eq!(iter.next(), Some(2));
        assert_eq!(iter.next(), None);
    }

    #[test]
    fn test_append() {
        let numbers = [1, 2, 3];
        let mut list1 = DoublyLinkedList::new();
        let mut list2 = DoublyLinkedList::from_iter(numbers);
        assert_eq!(list2.len(), numbers.len());
        list1.append(&mut list2);
        assert_eq!(list1.len(), numbers.len());
        assert!(list2.is_empty());
    }

    #[test]
    fn test_prepend() {
        let numbers = [1, 2, 3];
        let mut list1 = DoublyLinkedList::new();
        list1.push_back(4);
        let mut list2 = DoublyLinkedList::from_iter(numbers);
        assert_eq!(list2.len(), numbers.len());
        list1.prepend(&mut list2);
        assert!(list2.is_empty());
        assert_eq!(list1.len(), numbers.len() + 1);
        assert_eq!(list1, DoublyLinkedList::from_iter([1, 2, 3, 4]));
    }

    #[test]
    fn test_extend() {
        let mut list = DoublyLinkedList::new();
        let numbers = [1, 2, 3];
        list.extend(numbers);
        assert_eq!(list, DoublyLinkedList::from_iter(numbers));
    }

    #[test]
    fn test_insert() {
        let mut list = DoublyLinkedList::new();
        list.insert_at(0, 1);
        list.insert_at(0, 0);
        list.insert_at(2, 3);
        list.insert_at(2, 2);
        assert_eq!(list.into_iter().collect::<Vec<_>>(), [0, 1, 2, 3]);
    }

    #[test]
    fn test_insert_range() {
        let mut list = DoublyLinkedList::new();
        list.push_back(0);
        list.push_back(3);
        list.insert_iter(1, [1, 2]);
        assert_eq!(list, DoublyLinkedList::from_iter([0, 1, 2, 3]));
    }

    #[test]
    fn test_contains() {
        let list = DoublyLinkedList::from_iter([1, 2, 3, 4, 5]);
        assert!(list.contains(&3));
        assert!(list.contains(&4));
        assert!(!list.contains(&6));
        assert!(!list.contains(&0));
    }

    #[test]
    fn test_pop() {
        let mut list = DoublyLinkedList::from_iter([1, 2, 3, 4]);
        list.pop(&2);
        assert_eq!(list.len(), 3);
    }

    #[test]
    fn test_pop_at() {
        let mut list = DoublyLinkedList::from_iter([1, 2, 3, 4]);
        let ret = list.pop_at(1);
        assert_eq!(ret, Some(2));
        assert_eq!(list.len(), 3);
    }

    #[test]
    fn test_pop_if() {
        let mut list = DoublyLinkedList::from_iter([1, 2, 3, 4]);
        let ret = list.pop_if(|value| value % 2 == 0);
        assert_eq!(ret, Some(2));
        assert_eq!(list.len(), 3);
        let ret = list.pop_if(|value| value % 2 == 0);
        assert_eq!(ret, Some(4));
        assert_eq!(list.len(), 2);
    }

    #[test]
    fn test_unique() {
        let mut list = DoublyLinkedList::from_iter([1, 1, 2, 2, 3, 1, 1, 2]);
        let expected = [1, 2, 3, 1, 2];
        let ret = list.unique();
        assert_eq!(ret, 3);
        assert_eq!(list.into_iter().collect::<Vec<_>>(), expected);
    }

    #[test]
    fn test_reverse() {
        let mut list = DoublyLinkedList::from_iter([1, 2, 3, 4]);
        assert_eq!(list.iter().copied().collect::<Vec<_>>(), [1, 2, 3, 4]);
        list.reverse();
        assert_eq!(list.into_iter().collect::<Vec<_>>(), [4, 3, 2, 1]);
    }
}
}

环状双链表

与上文提到的双链表相比, 环状双向链表只是连接了链表的头部节点和尾部节点, 其它操作都差不多.

其结构如下图所示:

doubly circular linked list

算法实现

Header Linked List

Unrolled Linked List

参考

List of Lists

多层链表 Multi-level Linked List

多层链表 Multi-level linked list, 又称为十字链表 Orthogonal linked list.

可以使用它存储稀疏矩阵和图等数据结构.

链表中的每个节点包括两个指针, 分别指向左右水平的邻接节点和上下垂直邻结节点. 如果使用双链表风格的话, 每个节点会包含四个指针.

十字链表的应用: 十字链表最常见的应用是稀疏矩阵表示. 简而言之, 稀疏矩阵是其中大多数元素为零 (或任何已知常数) 的矩阵. 它们经常出现在科学应用中, 将稀疏矩阵表示为二维数组会浪费大量内存; 相反地, 稀疏矩阵表示为十字链表. 我们仅为矩阵中的非零元素创建一个节点, 并且在每个节点中存储值, 行索引和列索引以及指向其他节点的必要指针. 这节省了大量性能开销, 并且是实现稀疏矩阵最节省内存的方法.

单链表风格

每个节点只存储两个指针, 分别指向右侧和下层相邻的节点.

双链表风格

每个节点存储四个指针, 分别指向左右水平的邻接节点和上下垂直邻结节点.

跳跃表 Skip List

参考

跳跃表的基本操作

跳跃表的实现

跳跃表的应用

栈 Stacks

栈 (stack) 是一个线性数据结构, 内部存放的元素依照先入后出 (last in first out, FILO) 的顺序进行操作. 即先放入栈的元素离栈底较近, 出栈顺序比较晚, 后放入栈的元素离栈顶较近, 出栈顺序较早.

栈的基本结构如下图所示:

stack

跟据栈的大小可以分类为:

  • 固定大小的栈: 一旦初始化完成, 便不允许调整栈的大小. 如果当前栈已满, 再向栈顶加入新元素时就会触发 栈已满的错误; 如果当前栈是空的, 调用pop() 进行出栈时, 就会触发栈已空的错误
  • 可以动态调整大小的栈: 可以根据需要进行扩容或者缩容

栈的基本操作

栈的基本操作 ADT, 包括:

  • fn new(capacity) -> Stack: 初始化栈, 指定栈的大小
  • fn push(value: T) -> Result<(), T>: 将一个元素加入到栈顶
  • fn pop() -> Option<T>: 从栈顶移出一个元素
  • fn top() -> Option<&T>: 返回栈顶的元素, 但并不移除它
  • fn is_empty() -> bool: 检查栈是否为空
  • fn len() -> usize: 返回当前栈中包含的元素个数
  • fn capacity() -> usize: 对于静态栈, 返回栈的最大容量

要实现的 traits 有这些:

  • FromIterator<T>: 从迭代器构造栈, 如果是静态栈, 其容量大小就是迭代器中包含的元素个数
  • PartialEq<T>, Eq<T>, PartialOrd<T>, Ord<T>, 比较操作
  • Hash<T>: 支持哈稀函数

入栈 push()

将一个元素入栈:

  • 如果栈已满, 就不能再插入新的元素了, 返回栈已满的错误
  • 将栈顶的索引值 top 加上 1, 并将新元素加入到栈顶的位置

stack push

如果是动态栈, 不受容量限制, 那这个函数就没有返回值, 也不存在栈满的问题.

出栈 pop()

元素出栈顺序跟其入栈顺序是相反的.

从栈顶移出元素:

  • 如果栈已空, 就直接返回 None
  • 将栈顶的索引值 top 减去1, 并返回旧的栈顶元素

stack pop

返回栈顶的元素 top()

返回栈顶元素:

  • 返回之前先检查栈是否为空, 如果为空, 就直接返回栈空的错误
  • 返回当前的栈顶元素, 对栈不做任何改动

stack top

检查栈是否为空 is_empty()

  • 检查栈里的 top 的值
  • 如果 top == 0, 则说明栈为空, 返回 true
  • 否则栈中存有元素, 不为空, 返回 false

stack is empty

检查栈中当前的元素个数 len()

直接返回 len 属性

检查栈的容量 capacity()

直接返回 capacity 属性

栈的实现

使用数组实现

使用数组实现的栈结构, 它能保存的元素个数是固定的, 需要在初始化栈时指定栈的容量.

这里, 我们使用 Box<[Option<T>]> 用于指示数组中是否存储了元素, 如果它为 None 则表示在位置没有元素.

另外一种实现方式是 Box<[T]>, 并且要求类型 T 实现 Clone trait.

array stack

#![allow(unused)]
fn main() {
use std::cmp::Ordering;
use std::fmt;
use std::fmt::Formatter;
use std::hash::{Hash, Hasher};

/// 使用数组实现静态栈结构
pub struct ArrayStack<T> {
    top: usize,
    buf: Box<[Option<T>]>,
}

impl<T> ArrayStack<T> {
    /// 初始化栈, 指定栈的容量
    #[must_use]
    pub fn new(capacity: usize) -> Self {
        debug_assert!(capacity > 0);
        
        let values: Vec<Option<T>> = (0..capacity).map(|_| None).collect();

        Self {
            top: 0,
            buf: values.into_boxed_slice(),
        }
    }

    /// 将元素入栈
    ///
    /// # Errors
    ///
    /// 当栈已满时再将元素入栈, 就会返回错误, 以及原有的元素 `value`.
    pub fn push(&mut self, value: T) -> Result<(), T> {
        if self.top >= self.buf.len() {
            return Err(value);
        }
        self.buf[self.top] = Some(value);
        self.top += 1;
        Ok(())
    }

    /// 将栈顶元素出栈
    ///
    /// 当栈已经空时, 返回 `None`
    pub fn pop(&mut self) -> Option<T> {
        if self.top > 0 {
            self.top -= 1;
            self.buf[self.top].take()
        } else {
            None
        }
    }

    /// 返回栈顶元素
    #[must_use]
    pub const fn top(&self) -> Option<&T> {
        if self.top > 0 {
            self.buf[self.top - 1].as_ref()
        } else {
            None
        }
    }

    /// 检查栈是否空
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.top == 0
    }

    /// 返回当前栈中的元素个数
    #[must_use]
    pub const fn len(&self) -> usize {
        self.top
    }

    /// 返回栈的容量
    #[must_use]
    pub const fn capacity(&self) -> usize {
        self.buf.len()
    }
}

impl<T: PartialEq> PartialEq for ArrayStack<T> {
    fn eq(&self, other: &Self) -> bool {
        self.top == other.top && PartialEq::eq(&self.buf, &other.buf)
    }
}

impl<T: Eq> Eq for ArrayStack<T> {}

impl<T: PartialOrd> PartialOrd for ArrayStack<T> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        PartialOrd::partial_cmp(&self.buf, &other.buf)
    }
}

impl<T: Ord> Ord for ArrayStack<T> {
    fn cmp(&self, other: &Self) -> Ordering {
        Ord::cmp(&self.buf, &other.buf)
    }
}

impl<T: Hash> Hash for ArrayStack<T> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        Hash::hash(&self.buf, state);
    }
}

impl<T> FromIterator<T> for ArrayStack<T> {
    fn from_iter<U: IntoIterator<Item=T>>(iter: U) -> Self {
        let vec: Vec<Option<T>> = iter.into_iter().map(|item| Some(item)).collect();
        Self {
            top: vec.len(),
            buf: vec.into_boxed_slice(),
        }
    }
}

impl<T: fmt::Debug> fmt::Debug for ArrayStack<T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&self.buf, f)
    }
}
}

使用数组实现 - 消除 Option

上面的实现过程中, 使用了 Option<T> 来向数组中存储元素, 这会额外占用一些内存, 操作效率有影响. 我们可以手动操作内存, 来消除 Option<T>:

#![allow(unused)]
fn main() {
use std::{fmt, ptr};
use std::alloc::{alloc, Layout};
use std::cmp::Ordering;
use std::fmt::Formatter;
use std::hash::{Hash, Hasher};
use std::mem::ManuallyDrop;
use std::ptr::NonNull;

/// 使用数组实现静态栈结构
pub struct ArrayStack2<T> {
    top: usize,
    buf: Box<[T]>,
}

struct RawVec<T> {
    ptr: NonNull<T>,
    cap: usize,
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum AllocError {
    CapacityOverflow,
    AllocateError,
}

impl<T> ArrayStack2<T> {
    /// # Panics
    ///
    /// Raise panic if failed to allocate memory.
    #[must_use]
    pub fn new(capacity: usize) -> Self {
        debug_assert!(capacity > 0);

        let raw_vec = RawVec::<T>::try_allocate(capacity).expect("Failed to allocate buffer");
        let buf: Box<[T]> = unsafe { raw_vec.into_box() };

        Self {
            top: 0,
            buf,
        }
    }

    /// # Errors
    ///
    /// 当栈已满时再将元素入栈, 就会返回错误, 以及原有的元素 `value`.
    pub fn push(&mut self, value: T) -> Result<(), T> {
        if self.top >= self.buf.len() {
            return Err(value);
        }
        self.buf[self.top] = value;
        self.top += 1;
        Ok(())
    }

    pub fn pop(&mut self) -> Option<T> {
        if self.top > 0 {
            self.top -= 1;
            unsafe {
                Some(ptr::read(self.buf.as_ptr().wrapping_add(self.top)))
            }
        } else {
            None
        }
    }

    #[must_use]
    pub const fn top(&self) -> Option<&T> {
        if self.top > 0 {
            Some(&self.buf[self.top - 1])
        } else {
            None
        }
    }

    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.top == 0
    }

    #[must_use]
    pub const fn len(&self) -> usize {
        self.top
    }

    #[must_use]
    pub const fn capacity(&self) -> usize {
        self.buf.len()
    }
}

impl<T: PartialEq> PartialEq for ArrayStack2<T> {
    fn eq(&self, other: &Self) -> bool {
        self.top == other.top && PartialEq::eq(&self.buf, &other.buf)
    }
}

impl<T: Eq> Eq for ArrayStack2<T> {}

impl<T: PartialOrd> PartialOrd for ArrayStack2<T> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        PartialOrd::partial_cmp(&self.buf, &other.buf)
    }
}

impl<T: Ord> Ord for ArrayStack2<T> {
    fn cmp(&self, other: &Self) -> Ordering {
        Ord::cmp(&self.buf, &other.buf)
    }
}

impl<T: Hash> Hash for ArrayStack2<T> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        Hash::hash(&self.buf, state);
    }
}

impl<T> FromIterator<T> for ArrayStack2<T> {
    fn from_iter<U: IntoIterator<Item=T>>(iter: U) -> Self {
        let vec: Vec<T> = iter.into_iter().collect();
        Self {
            top: vec.len(),
            buf: vec.into_boxed_slice(),
        }
    }
}

impl<T: fmt::Debug> fmt::Debug for ArrayStack2<T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&self.buf, f)
    }
}


impl<T> RawVec<T> {
    fn try_allocate(
        capacity: usize,
    ) -> Result<Self, AllocError> {
        debug_assert!(capacity > 0);
        let Ok(layout) = Layout::array::<T>(capacity) else {
            return Err(AllocError::CapacityOverflow);
        };

        let ptr = unsafe { alloc(layout) };
        if ptr.is_null() {
            return Err(AllocError::AllocateError);
        }
        let ptr = unsafe {
            NonNull::new_unchecked(ptr.cast::<T>())
        };

        Ok(Self { ptr, cap: capacity })
    }

    unsafe fn into_box(self) -> Box<[T]> {
        let me = ManuallyDrop::new(self);
        unsafe {
            let slice = ptr::slice_from_raw_parts_mut(me.ptr.as_ptr(), me.cap);
            Box::from_raw(slice)
        }
    }
}
}

使用动态数组 Vec 实现动态栈

使用 Vec<T> 实现的栈可以进行动态扩容, 但每次扩容时可能要进行内存的批量拷贝.

这个比较简单, 因为 Vec<T> 本身就实现了基本的栈操作接口, 我们只需要再包装一下就行:

#![allow(unused)]
fn main() {
use std::cmp::Ordering;
use std::fmt;
use std::fmt::Formatter;
use std::hash::{Hash, Hasher};

pub struct VecStack<T: Sized>(Vec<T>);

impl<T> Default for VecStack<T> {
    #[inline]
    fn default() -> Self {
        Self::new()
    }
}

impl<T> VecStack<T> {
    /// 初始化栈, 默认的容量为 0
    #[must_use]
    #[inline]
    pub const fn new() -> Self {
        Self(Vec::new())
    }

    /// 初始化栈, 指定栈的容量, 但可以自动扩容.
    #[must_use]
    #[inline]
    pub fn with_capacity(capacity: usize) -> Self {
        Self(Vec::with_capacity(capacity))
    }

    /// 将元素入栈
    #[inline]
    pub fn push(&mut self, value: T) {
        self.0.push(value);
    }

    /// 将栈顶元素出栈
    ///
    /// 当栈已经空时, 返回 `None`
    #[inline]
    pub fn pop(&mut self) -> Option<T> {
        self.0.pop()
    }

    /// 返回栈顶元素
    #[must_use]
    #[inline]
    pub fn top(&self) -> Option<&T> {
        self.0.last()
    }

    /// 检查栈是否空
    #[must_use]
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// 返回当前栈中的元素个数
    #[must_use]
    #[inline]
    pub fn len(&self) -> usize {
        self.0.len()
    }

    /// 返回栈的容量
    #[must_use]
    #[inline]
    pub fn capacity(&self) -> usize {
        self.0.capacity()
    }
}


impl<T: PartialEq> PartialEq for VecStack<T> {
    fn eq(&self, other: &Self) -> bool {
        PartialEq::eq(&self.0, &other.0)
    }
}

impl<T: Eq> Eq for VecStack<T> {}

impl<T: PartialOrd> PartialOrd for VecStack<T> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        PartialOrd::partial_cmp(&self.0, &other.0)
    }
}

impl<T: Ord> Ord for VecStack<T> {
    fn cmp(&self, other: &Self) -> Ordering {
        Ord::cmp(&self.0, &other.0)
    }
}

impl<T: Hash> Hash for VecStack<T> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        Hash::hash(&self.0, state);
    }
}

impl<T> FromIterator<T> for VecStack<T> {
    fn from_iter<U: IntoIterator<Item=T>>(iter: U) -> Self {
        let vec: Vec<T> = iter.into_iter().collect();
        Self(vec)
    }
}

impl<T: fmt::Debug> fmt::Debug for VecStack<T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&self.0, f)
    }
}
}

使用链表实现动态栈

使用链表实现动态栈, 也是一个可行的方式, 为了简化代码, 我们使用了标准库中的双链表. 但是在这里使用单链表就足够了.

#![allow(unused)]
fn main() {
use std::cmp::Ordering;
use std::collections::LinkedList;
use std::fmt;
use std::fmt::Formatter;
use std::hash::{Hash, Hasher};

#[allow(clippy::linkedlist)]
pub struct ListStack<T> (LinkedList<T>);

impl<T> ListStack<T> {
    #[must_use]
    #[inline]
    pub const fn new() -> Self {
        Self(LinkedList::new())
    }


    /// 将元素入栈
    #[inline]
    pub fn push(&mut self, value: T) {
        self.0.push_back(value);
    }

    /// 将栈顶元素出栈
    ///
    /// 当栈已经空时, 返回 `None`
    #[must_use]
    #[inline]
    pub fn pop(&mut self) -> Option<T> {
        self.0.pop_back()
    }

    /// 返回栈顶元素
    #[must_use]
    #[inline]
    pub fn top(&self) -> Option<&T> {
        self.0.back()
    }

    #[must_use]
    #[inline]
    pub fn len(&self) -> usize {
        self.0.len()
    }

    #[must_use]
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }
}

impl<T> Default for ListStack<T> {
    #[inline]
    fn default() -> Self {
        Self::new()
    }
}

impl<T: PartialEq> PartialEq for ListStack<T> {
    fn eq(&self, other: &Self) -> bool {
        PartialEq::eq(&self.0, &other.0)
    }
}

impl<T: Eq> Eq for ListStack<T> {}

impl<T: PartialOrd> PartialOrd for ListStack<T> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        PartialOrd::partial_cmp(&self.0, &other.0)
    }
}

impl<T: Ord> Ord for ListStack<T> {
    fn cmp(&self, other: &Self) -> Ordering {
        Ord::cmp(&self.0, &other.0)
    }
}

impl<T: Hash> Hash for ListStack<T> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        Hash::hash(&self.0, state);
    }
}

impl<T> FromIterator<T> for ListStack<T> {
    fn from_iter<U: IntoIterator<Item=T>>(iter: U) -> Self {
        let list: LinkedList<T> = iter.into_iter().collect();
        Self(list)
    }
}

impl<T: fmt::Debug> fmt::Debug for ListStack<T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&self.0, f)
    }
}
}

栈的应用

栈是一个线性的数据结构. 它在编程领域有广泛的使用. 比如操作系统会为每个线程分配一个函数调用栈, 用于保存函数内的局部变量.

stack

常见的栈的应用有:

  • 函数调用: 用于记录函数返回地址, 当被调用函数执行完后可以将返回值正确还回给函数调用处
  • 递归: 递归函数的调用, 通常可以将它们转换成迭代的形式, 这时可以利用栈来存放每次递归调用时的值
  • 语法解析: 可以利用栈来检验编程语言中表达式的语法
  • 表达式求值: 可以用栈来实现对后缀表达式的求值

单调栈 Monotonic Stack

队列 Queues

队列是计算机科学里的基础的概念, 它用于以一定顺序存储和管理数据.

它遵循"先进先出" (First In First Out, FIFO) 的原则, 即先进入到队列里的元素会先出队列. 它是两端开口的线性数据结构 (linear data structure),

基本的操作如下图所示:

queue data flow

队列的分类

根据队列可以容纳的元素个数不同, 可以被分为:

  • 静态队列, 或者固定队列: 即队列在初始化时就指定它的容量, 队列中的元素个数不同超过该容量, 否则就出队 (enqueue) 失败
  • 动态队列: 即队列中的元素个数不受限制

根据其结构不同, 队列可以分成几种类型:

  • 简单队列 simple queue: 从一端入队 (enqueue), 而从另一端出队 (dequeue)
  • 双端队列 double-ended queue(deque): 左右两端都可以入队出队
    • 限制入队队列 input-restricted queue: 元素可以从两端出队, 但只能从一端入队
    • 限制出队队列 output-restricted queue: 元素可以从两端入队, 但只能从一端出队
  • 环形队列 circular queue: 又称为环状缓冲区, 整个队列的队首与队尾相连, 元素只从队列的头部出队, 从队列的尾部入队
  • 优先级队列 priority queue: 队列中的元素按照某个规则升序或者降序依次排列

因为 双端队列优先级队列 比较复杂, 在后面有单独的章节介绍它们, 本章内容不再提及.

队列的基本操作

队列的基本接口包括:

  • fn new() -> Self, 创建一个动态队列, 其容量不受限制
  • fn new(capacity) -> Self, 创建一个静态队列, 初始化时就指定队列的容量
  • fn len() -> usize, 返回当前队列中的元素个数
  • fn capacity() -> usize, 对于静态队列, 返回队列中的容量
  • fn is_empty() -> bool, 对于静态队列, 查看队列是否已满
  • fn front() -> Option<&T>, 返回队列头部元素的共享引用, 如果有的话
  • fn front_mut() -> Option<&mut T>, 返回队列头部元素的可变引用, 如果有的话
  • fn back() -> Option<&T>, 返回队列尾部元素的共享引用, 如果有的话
  • fn back_mut() -> Option<&mut T>, 返回队列尾部元素的可变引用, 如果有的话
  • fn push(value: T) -> Result<(), T>, 简单队列需要实现的接口, 从队列的一端插入元素
  • fn pop() -> Option<T>, 简单队列需要实现的接口, 从队列的另一端弹出元素

要实现的 traits 有这些:

  • FromIterator<T>: 从迭代器构造隐列, 如果是静态队列, 其容量大小就是迭代器中包含的元素个数
  • PartialEq<T>, Eq<T>, PartialOrd<T>, Ord<T>, 比较操作
  • Hash<T>: 支持哈稀函数

实现简单队列

使用数组实现

对于有静态队列, 使用数组来实现比较符合直觉.

#![allow(unused)]
fn main() {
use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};

pub struct ArrayQueue<T> {
    len: usize,
    buf: Box<[Option<T>]>,
}

impl<T> ArrayQueue<T> {
    #[must_use]
    pub fn new(capacity: usize) -> Self {
        let values: Vec<Option<T>> = (0..capacity).map(|_| None).collect();

        Self {
            len: 0,
            buf: values.into_boxed_slice(),
        }
    }

    /// # Errors
    ///
    /// 当栈已满时再将元素入队, 就会返回错误, 以及原有的元素 `value`.
    pub fn push(&mut self, value: T) -> Result<(), T> {
        if self.len == self.buf.len() {
            return Err(value);
        }

        self.buf[self.len] = Some(value);
        self.len += 1;

        Ok(())
    }

    pub fn pop(&mut self) -> Option<T> {
        if self.len > 0 {
            let front = self.buf[0].take();
            for i in 1..self.len {
                self.buf.swap(i - 1, i);
            }
            self.len -= 1;
            front
        } else {
            None
        }
    }

    #[must_use]
    #[inline]
    pub const fn len(&self) -> usize {
        self.len
    }

    #[must_use]
    #[inline]
    pub const fn is_empty(&self) -> bool {
        self.len == 0
    }

    #[must_use]
    #[inline]
    pub const fn capacity(&self) -> usize {
        self.buf.len()
    }

    #[must_use]
    #[inline]
    pub const fn front(&self) -> Option<&T> {
        if self.len > 0 {
            self.buf[0].as_ref()
        } else {
            None
        }
    }

    #[must_use]
    #[inline]
    pub fn front_mut(&mut self) -> Option<&mut T> {
        if self.len > 0 {
            self.buf[0].as_mut()
        } else {
            None
        }
    }

    #[must_use]
    #[inline]
    pub const fn back(&self) -> Option<&T> {
        if self.len > 0 {
            self.buf[self.len - 1].as_ref()
        } else {
            None
        }
    }

    #[must_use]
    #[inline]
    pub fn back_mut(&mut self) -> Option<&mut T> {
        if self.len > 0 {
            self.buf[self.len - 1].as_mut()
        } else {
            None
        }
    }
}

impl<T: PartialEq> PartialEq for ArrayQueue<T> {
    fn eq(&self, other: &Self) -> bool {
        self.len == other.len && PartialEq::eq(&self.buf, &other.buf)
    }
}

impl<T: Eq> Eq for ArrayQueue<T> {}

impl<T: PartialOrd> PartialOrd for ArrayQueue<T> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        PartialOrd::partial_cmp(&self.buf, &other.buf)
    }
}

impl<T: Ord> Ord for ArrayQueue<T> {
    fn cmp(&self, other: &Self) -> Ordering {
        Ord::cmp(&self.buf, &other.buf)
    }
}

impl<T: Hash> Hash for ArrayQueue<T> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        Hash::hash(&self.buf, state);
    }
}

impl<T> FromIterator<T> for ArrayQueue<T> {
    fn from_iter<U: IntoIterator<Item=T>>(iter: U) -> Self {
        let vec: Vec<Option<T>> = iter.into_iter().map(|item| Some(item)).collect();
        Self {
            len: vec.len(),
            buf: vec.into_boxed_slice(),
        }
    }
}

impl<T: fmt::Debug> fmt::Debug for ArrayQueue<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&self.buf, f)
    }
}

使用数组实现 - 消除 Option<T> 类型

上面中的队列, 使用了 [Option<T>] 来表示数组中的元素类型, 这有些占用空间, 我们可以将这个问题消除, 通过手动操作内存的方式. 当然这会引入 unsafe 的函数:

#![allow(unused)]
fn main() {
use std::{fmt, ptr};
use std::alloc::{alloc, Layout};
use std::cmp::Ordering;
use std::hash::{Hash, Hasher};
use std::mem::ManuallyDrop;
use std::ptr::NonNull;

pub struct ArrayQueue2<T> {
    len: usize,
    buf: Box<[T]>,
}

struct RawVec<T> {
    ptr: NonNull<T>,
    cap: usize,
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum AllocError {
    CapacityOverflow,
    AllocateError,
}

impl<T> ArrayQueue2<T> {
    /// # Panics
    ///
    /// Raise panic if failed to allocate memory.
    #[must_use]
    pub fn new(capacity: usize) -> Self {
        assert!(capacity > 0);
        let raw_vec = RawVec::<T>::try_allocate(capacity).expect("Failed to allocate buffer");
        let buf: Box<[T]> = unsafe { raw_vec.into_box() };
        Self {
            len: 0,
            buf,
        }
    }

    /// # Errors
    ///
    /// 当栈已满时再将元素入队, 就会返回错误, 以及原有的元素 `value`.
    pub fn push(&mut self, value: T) -> Result<(), T> {
        if self.len == self.buf.len() {
            return Err(value);
        }

        self.buf[self.len] = value;
        self.len += 1;

        Ok(())
    }

    pub fn pop(&mut self) -> Option<T> {
        if self.len > 0 {
            // Take the first value, without calling drop method.
            let front = unsafe {
                Some(ptr::read(self.buf.as_ptr()))
            };
            // Move memory.
            unsafe {
                ptr::copy(self.buf.as_ptr().wrapping_add(1), self.buf.as_mut_ptr(), self.len - 1);
            }
            self.len -= 1;
            front
        } else {
            None
        }
    }

    #[must_use]
    #[inline]
    pub const fn len(&self) -> usize {
        self.len
    }

    #[must_use]
    #[inline]
    pub const fn is_empty(&self) -> bool {
        self.len == 0
    }

    #[must_use]
    #[inline]
    pub const fn capacity(&self) -> usize {
        self.buf.len()
    }

    #[must_use]
    #[inline]
    pub const fn front(&self) -> Option<&T> {
        if self.len > 0 {
            Some(&self.buf[0])
        } else {
            None
        }
    }

    #[must_use]
    #[inline]
    pub fn front_mut(&mut self) -> Option<&mut T> {
        if self.len > 0 {
            Some(&mut self.buf[0])
        } else {
            None
        }
    }

    #[must_use]
    #[inline]
    pub const fn back(&self) -> Option<&T> {
        if self.len > 0 {
            Some(&self.buf[self.len - 1])
        } else {
            None
        }
    }

    #[must_use]
    #[inline]
    pub fn back_mut(&mut self) -> Option<&mut T> {
        if self.len > 0 {
            Some(&mut self.buf[self.len - 1])
        } else {
            None
        }
    }
}

impl<T: PartialEq> PartialEq for ArrayQueue2<T> {
    fn eq(&self, other: &Self) -> bool {
        self.len == other.len && PartialEq::eq(&self.buf, &other.buf)
    }
}

impl<T: Eq> Eq for ArrayQueue2<T> {}

impl<T: PartialOrd> PartialOrd for ArrayQueue2<T> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        PartialOrd::partial_cmp(&self.buf, &other.buf)
    }
}

impl<T: Ord> Ord for ArrayQueue2<T> {
    fn cmp(&self, other: &Self) -> Ordering {
        Ord::cmp(&self.buf, &other.buf)
    }
}

impl<T: Hash> Hash for ArrayQueue2<T> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        Hash::hash(&self.buf, state);
    }
}

impl<T> FromIterator<T> for ArrayQueue2<T> {
    fn from_iter<U: IntoIterator<Item=T>>(iter: U) -> Self {
        let vec: Vec<T> = iter.into_iter().collect();
        Self {
            len: vec.len(),
            buf: vec.into_boxed_slice(),
        }
    }
}

impl<T: fmt::Debug> fmt::Debug for ArrayQueue2<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&self.buf, f)
    }
}

impl<T> RawVec<T> {
    fn try_allocate(
        capacity: usize,
    ) -> Result<Self, AllocError> {
        debug_assert!(capacity > 0);
        let Ok(layout) = Layout::array::<T>(capacity) else {
            return Err(AllocError::CapacityOverflow);
        };

        let ptr = unsafe { alloc(layout) };
        if ptr.is_null() {
            return Err(AllocError::AllocateError);
        }
        let ptr = unsafe {
            NonNull::new_unchecked(ptr.cast::<T>())
        };

        Ok(Self { ptr, cap: capacity })
    }

    unsafe fn into_box(self) -> Box<[T]> {
        let me = ManuallyDrop::new(self);
        unsafe {
            let slice = ptr::slice_from_raw_parts_mut(me.ptr.as_ptr(), me.cap);
}

使用链表实现

可以使用链表来实现动态数组, 不限制队列中的元素个数.

对标准库中的双链表, 就可以很容易支持队列的接口.

#![allow(unused)]
fn main() {
use std::cmp::Ordering;
use std::collections::LinkedList;
use std::fmt;
use std::hash::{Hash, Hasher};

#[allow(clippy::linkedlist)]
pub struct ListQueue<T> (LinkedList<T>);

impl<T> Default for ListQueue<T> {
    #[inline]
    fn default() -> Self {
        Self::new()
    }
}

impl<T> ListQueue<T> {
    #[must_use]
    #[inline]
    pub const fn new() -> Self {
        Self(LinkedList::new())
    }

    #[inline]
    pub fn push(&mut self, value: T) {
        self.0.push_back(value);
    }

    #[must_use]
    #[inline]
    pub fn pop(&mut self) -> Option<T> {
        self.0.pop_front()
    }

    #[must_use]
    #[inline]
    pub fn len(&self) -> usize {
        self.0.len()
    }

    #[must_use]
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    #[must_use]
    #[inline]
    pub fn front(&self) -> Option<&T> {
        self.0.front()
    }

    #[must_use]
    #[inline]
    pub fn front_mut(&mut self) -> Option<&mut T> {
        self.0.front_mut()
    }

    #[must_use]
    #[inline]
    pub fn back(&self) -> Option<&T> {
        self.0.back()
    }

    #[must_use]
    #[inline]
    pub fn back_mut(&mut self) -> Option<&mut T> {
        self.0.back_mut()
    }
}

impl<T: PartialEq> PartialEq for ListQueue<T> {
    fn eq(&self, other: &Self) -> bool {
        PartialEq::eq(&self.0, &other.0)
    }
}

impl<T: Eq> Eq for ListQueue<T> {}

impl<T: PartialOrd> PartialOrd for ListQueue<T> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        PartialOrd::partial_cmp(&self.0, &other.0)
    }
}

impl<T: Ord> Ord for ListQueue<T> {
    fn cmp(&self, other: &Self) -> Ordering {
        Ord::cmp(&self.0, &other.0)
    }
}

impl<T: Hash> Hash for ListQueue<T> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        Hash::hash(&self.0, state);
    }
}

impl<T> FromIterator<T> for ListQueue<T> {
    fn from_iter<U: IntoIterator<Item=T>>(iter: U) -> Self {
        let list = iter.into_iter().collect();
        Self(list)
    }
}

impl<T: fmt::Debug> fmt::Debug for ListQueue<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&self.0, f)
    }
}
}

环形缓冲区 Circular Buffer

环形缓冲区 Circular Buffer 又称为 Ring Buffer, Cyclic Buffer 或者 Circular Queue.

环形缓冲区是线性数据结构, 通常由数组来实现, 如下图所示:

circular buffer linear

将尾部与头部相连, 组成一个环形索引, 逻辑上的关系如下图所示.:

circular buffer circular

所以才称为环形缓冲区.

环形缓冲区实现的是单生产者-单消费者模式 (single-producer, single-consumer), 生产者将元素加到尾部, 然后消费者从头部读取元素, FIFO (first in first out).

与链表相比, 这种数据结构更加紧凑, 空间利用率高, 对CPU的缓存友好, 常用作 I/O buffering.

环形缓冲区的基本操作

TODO(Shaohua):

初始化缓冲区

因为缓冲区的容量是事先确定的, 在初始化它的同时, 可以分配好相应的堆内存. 如果分配内存失败, 就直接产生 panic 异常.

函数签名是: pub fn new(capacity: usize) -> Self

向缓冲区中加入元素

函数签名是: pub fn push(&mut self, value: T) -> Result<(), T>

生产者调用它, 加入元素时, 如果缓冲区已经满了, 就直接返回 Err(value). 为了简化实现, 我们并没有定义相应的 错误类型.

从缓冲区中读取元素

消费者调用它, 每次读取一个元素.

函数签名是: pub fn pop(&mut self) -> Option<T>

如果缓冲区已经空了, 就返回 None

环形缓冲区的实现

考虑到性能, 下面的 CircularBuffer 使用了几个 unsafe 接口, 要特别留意指针的操作.

#![allow(unused)]
fn main() {
use std::alloc::{alloc, dealloc, Layout};
use std::marker::PhantomData;
use std::ptr::NonNull;
use std::{mem, ops, ptr, slice};

pub struct CircularBuffer<T: Sized> {
    start: usize,
    len: usize,
    cap: usize,
    ptr: NonNull<T>,
    _marker: PhantomData<T>,
}

impl<T: Sized> CircularBuffer<T> {
    /// # Panics
    ///
    /// 分配内存失败时直接返回 panic
    #[must_use]
    #[inline]
    pub fn new(capacity: usize) -> Self {
        // 为了方便处理, 我们强制要求 capacity 是正数, 并且目前还没有考虑 ZST (zero sized type).
        assert!(capacity > 0);

        let layout = Layout::array::<T>(capacity).expect("Layout error");
        let ptr = unsafe { alloc(layout) };
        let ptr = NonNull::new(ptr).expect("Failed to alloc");

        Self {
            start: 0,
            len: 0,
            cap: capacity,
            ptr: ptr.cast(),
            _marker: PhantomData,
        }
    }

    #[must_use]
    #[inline]
    pub const fn as_mut_ptr(&self) -> *mut T {
        self.ptr.as_ptr()
    }

    #[must_use]
    #[inline]
    pub const fn as_ptr(&self) -> *const T {
        self.ptr.as_ptr()
    }

    #[must_use]
    #[inline]
    pub fn as_slice(&self) -> &[T] {
        self
    }

    #[must_use]
    #[inline]
    pub fn as_mut_slice(&mut self) -> &mut [T] {
        self
    }

    /// # Errors
    ///
    /// 当缓冲区已满时返回 `Err(value)`
    pub fn push(&mut self, value: T) -> Result<(), T> {
        if self.is_full() {
            Err(value)
        } else {
            unsafe {
                // 计算新元素的指针位置
                let end = (self.start + self.len) % self.cap;
                let end_ptr = self.as_mut_ptr().add(end);
                self.len += 1;
                ptr::write(end_ptr, value);
            }
            Ok(())
        }
    }

    /// 从缓冲区消费元素, 如果缓冲区已空, 就返回 `None`
    pub fn pop(&mut self) -> Option<T> {
        if self.is_empty() {
            None
        } else {
            unsafe {
                // 计算起始元素的地址
                let start_ptr = self.as_ptr().add(self.start);
                self.start = (self.start + 1) % self.cap;
                self.len -= 1;
                Some(ptr::read(start_ptr))
            }
        }
    }

    /// 返回当前缓冲区中的元素个数
    #[must_use]
    #[inline]
    pub const fn len(&self) -> usize {
        self.len
    }

    #[must_use]
    #[inline]
    pub const fn capacity(&self) -> usize {
        self.cap
    }

    #[must_use]
    #[inline]
    pub const fn is_empty(&self) -> bool {
        self.len() == 0
    }

    #[must_use]
    #[inline]
    pub const fn is_full(&self) -> bool {
        self.len() == self.cap
    }

    // 计算当前的内存结构
    fn current_memory(&self) -> (NonNull<u8>, Layout) {
        assert_eq!(mem::size_of::<T>() % mem::align_of::<T>(), 0);
        unsafe {
            let align = mem::align_of::<T>();
            let size = mem::size_of::<T>().unchecked_mul(self.cap);
            let layout = Layout::from_size_align_unchecked(size, align);
            (self.ptr.cast(), layout)
        }
    }
}

/// 释放堆内存
impl<T> Drop for CircularBuffer<T> {
    fn drop(&mut self) {
        let (ptr, layout) = self.current_memory();
        unsafe { dealloc(ptr.as_ptr(), layout) }
    }
}

/// 实现 `Deref` 和 `DerefMut` traits.
impl<T> ops::Deref for CircularBuffer<T> {
    type Target = [T];

    #[inline]
    fn deref(&self) -> &[T] {
        unsafe { slice::from_raw_parts(self.as_ptr(), self.len) }
    }
}

impl<T> ops::DerefMut for CircularBuffer<T> {
    #[inline]
    fn deref_mut(&mut self) -> &mut [T] {
        unsafe { slice::from_raw_parts_mut(self.as_mut_ptr(), self.len) }
    }
}

/// 支持从迭代器初始化.
impl<T> FromIterator<T> for CircularBuffer<T> {
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
        // 为了实现简单, 我们重用了 vec 的 `FromIterator` 实现.
        let vec: Vec<T> = iter.into_iter().collect();
        let len = vec.len();
        let cap = vec.capacity();
        let boxed = vec.into_boxed_slice();
        let ptr = Box::leak(boxed);
        let ptr = NonNull::new(ptr.as_mut_ptr()).unwrap();
        Self {
            start: 0,
            len,
            cap,
            ptr,
            _marker: PhantomData,
        }
    }
}
}

环形缓冲区的应用

有不少软件有使用它来管理缓冲区, 下面就列举几个.

比如, 在 linux 内核的网络栈, 接收到对方发送的数据包后, 就先放到对应的环形缓冲区, 并且根据它剩下的空间大小, 来通知发送方调整滑动窗口的大小.

参考

单调队列 Monotonic queue

TODO(Shaohua)

双端队列 Deque

双端队列的基本操作

双端队列的实现

标准库中 VecDeque 的实现

哈稀表 Hash Tables

标准库中 HashMap 的实现

标准库中 HashSet 的实现

LinkedHashMap

二叉树

二叉树的线性存储结构

二叉树的链式存储结构

二叉树的遍历 Traversal

二叉树的前序遍历 Pre-order Traversal

二叉树的中序遍历

二叉树的后序遍历

二叉树的层序遍历

二叉搜索树 Binary Search Tree

平衡二叉树 Optimal BST

AVL树

Splay Tree

红黑树 Red-Black Trees

Left-leaning Red–Black Tree

多叉树

参考

B-Trees

B+ Trees

B*-Tree

T-tree

参考

LSM tree

Fractal-tree

标准库中 BTreeMap 的实现

字典树 Trie

Radix Tree

参考

Suffix Tree

参考

R-tree

参考

R-tree

Priority R-tree

参考

R*-tree

参考

R+-tree

参考

X-tree

参考

K-d Tree

优先级队列 Priority Queues

Binary Heap

参考

双优先级队列 Dual Priority Queues

Skew Heap

Binomial Heap

参考

Brodal Queue

参考

Fibonacci Heap

参考

Strict Fibonacci Heap

参考

标准库中 Binary Heap 的实现

图算法

深度优先搜索

广度优先搜索

最短路径

最小生成树

并发数据结构 Concurrent Data Structures

简介

并发数据结构 Concurrent data structures, CDS 有三个关键的主题:

  • 安全 Safety, 满足多线程并发的规范
    • 顺序规则 sequential specification, 像一个队列一样按顺序操作
    • 同步 synchronization
  • 可扩展性 Scalability, 随着处理器核心数的增多, 性能更好
    • 理想情况下, 线性递增
    • 实际情况, 超过 16 个线程之后, 会退化成次线性递增 (sublinear scaling)
  • 有进度 Progress, 保证操作过程向前推进
    • lock freedom: 至少有一个进度向前推进
    • wait freedom: 所有的进度都向前推进

安全性 Safety

使用锁或者其它同步原语 (primitive synchronization) 来保护并发数据结构.

  • 使用全局锁来保护顺序数据结构
  • 使用自定义的同步协议来保护数据结构

可扩展性 Scalability

  • 减少锁保护的作用域
    • 读写锁 read-write locking
    • hand-over-hand locking
    • lock coupling
  • 避免写数据以便减少无效的缓存
    • 乐观锁

Lock-coupling Linked List

Concurrent Ring Buffer

Single-producer, single-consumer lock-free FIFO

Concurrent Hash Map

Concurrent List

Concurrent Deque

Concurrent Queue

Multi-producer, multi-consumer lock-free FIFO

参考

Concurrent SkipList Map

Concurrent SkipList Set

Concurrent Radix Tree

Skip Graph

参考

算法分析

测试用的数据集

排序 Sorting

所谓的数组排序, 就是按照相同的顺序将数组中所有元素依次排好.

排序算法的特点:

  • 稳定排序 (stable sort): 排序相同值的元素时, 会保持它们在数组中的原有顺序
  • 不稳定排序 (unstable sort): 排序相同值的元素时, 会打乱它们在数组中的原有顺序
  • adaptive sort: 能利用输入数组的已有顺序, 如果输入的是基本已排序好的数组, 排序效率更高
  • non-adaptive sort: 即使输入的数组已基本有序, 仍然需要固定的步骤完成排序工作, 所以排序效率较低
  • 原地排序 (in-place sort): 不需要额外的内存空间, 只在原先的数组上进行排序; 当然, 在交换元素时用到的一个临时变量不算在内

参考

冒泡排序 Bubble sort

该算法将数组分成了两个部分, 左侧部分是未排序的, 右侧部分是已排序好的.

排序的步骤

  1. 从左到右遍历数组, 比较相邻的元素, 将较大的元素放在右侧
  2. 重复这个过程, 这样最大的元素就会放在数组最右侧
  3. 重复步骤1-2, 找到第二大的元素, 并放在数组右侧第二个位置
  4. 直到没有元素需要被交换, 整个数组变得有序

下面以 arr = [9, 4, 1, 7]; 为例来进行演示.

第一阶段, 从左到右遍历数组, 找到最大的元素 9, 并将它放在数组最右侧.

bubble sort stage 1

第一阶段, 从左到右遍历数组, 找到最大的元素 7, 并将它放在数组右侧第二个位置.

bubble sort stage 2

第一阶段, 从左到右遍历数组, 发现数组已排序完成.

bubble sort stage 3

实现冒泡排序算法

#![allow(unused)]
fn main() {
/// 如果传入的数据是增序排好的, 那么只需要 `n-1` 次的比较, 以及 0 次的交换;
/// 平珓情况以及最坏情况下, 使用 `n^2 / 2` 次比较以及 `n^2 / 2` 次交换.
pub fn bubble_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    let len = arr.len();
    for i in 0..len {
        let mut swapped = false;
        // 以 (len - i - 1) 为分隔点, 左侧部分是无序的, 右侧部分是有序的
        for j in 0..(len - i - 1) {
            if arr[j] > arr[j + 1] {
                swapped = true;
                arr.swap(j, j + 1);
            }
        }

        // 如果没有元素需要交换, 说明左侧部分也是有序的
        if !swapped {
            break;
        }
    }
}
}

递归实现冒泡排序

根据上面的描述, 冒泡排序的第一步, 将最大的元素移到数组最右侧; 在第二步中, 将第二大的元素移到右侧第二位. 基于此, 就可以编写递归形式的冒泡排序算法:

  1. 如果数组长度为1, 就直接返回
  2. 将最大的元素移到数组最右侧
  3. 递归调用冒泡排序, 但忽略数组的最右侧元素

递归形式的冒泡排序算法需要额外占用 O(n) 的内存空间, 用于递归函数调用栈.

#![allow(unused)]
fn main() {
/// 递归形式的冒泡排序算法.
///
/// 与迭代形式的算法相比, 递归形式实现的算法, 并没有性能上的优势.
pub fn recursive_bubble_sort<T>(list: &mut [T])
where
    T: PartialOrd,
{
    let len = list.len();
    if len < 2 {
        return;
    }

    let mut swapped = false;
    for j in 0..(len - 1) {
        if list[j] > list[j + 1] {
            swapped = true;
            list.swap(j, j + 1);
        }
    }

    // 如果没有元素需要交换, 说明数组有序的
    if !swapped {
        return;
    }

    recursive_bubble_sort(&mut list[..(len - 1)]);
}
}

冒泡排序的特点

  • 时间复杂度是 O(n^2), 空间复杂度是 O(1)
  • 在交换元素时, 只与相邻的元素交换, 交换次数可能比较多
  • 属于稳定排序 (stable sort)
  • 是 adaptive sort
  • 比较适合已经基本排序好的数组, 可以显著提高排序效率; 对于已排序好的数组, 时间复杂度是 O(n)
  • 只适合元素比较少的数组

插入排序 Insertion sort

插入排序实现方法比较简单. 它一次排序一个元素, 将数组分成两部分, 左侧部分是有序的, 右侧部分是待排序的.

插入排序的步骤

  1. 从第二个元素开始遍历数组, 因为数组中的第一个元素是有序的
  2. 将第二个元素与第一个元素比较, 如果比第一个元素小, 就交换两者的位置
  3. 将第三个元素与第二个位置的元素比较, 如果比它小, 就交换位置, 并重复第2步
  4. 继续以上的步骤, 将无序元素与前面的有序的元素进行比较以及交换位置
  5. 重复操作, 直到整个数组都是有序的

第一阶段, 将第二个元素 4 与第一个元素 9 进行比较, 并交换位置:

insertion sort pass 1

第二阶段, 将第三个元素 1 与前面的元素比较, 并交换位置:

insertion sort pass 2

第三阶段, 将第四个元素 7 与前面的元素比较并交换位置:

insertion sort pass 3

插入排序的实现

#![allow(unused)]
fn main() {
/// 其思路是, 先将前 i 个元素调整为增序的, 随着 i 从 0 增大到 n, 整个序列就变得是增序了.
pub fn insertion_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    let len = arr.len();
    for i in 1..len {
        for j in (1..=i).rev() {
            if arr[j - 1] > arr[j] {
                arr.swap(j - 1, j);
            } else {
                break;
            }
        }
    }
}
}

递归实现插入排序

根据上面的描述, 插入排序会将数组分成两部分, 左侧部分是已经排好序的, 右侧部分是待排序的. 现在用递归的形式重新实现这个步骤:

  1. 对于第 k 个元素, 先将 list[0..k] 进行递归排序
  2. 然后将第 k 个元素与前面已经排序好的 list[0..k] 进行比较并交换位置, 以便让它放在合适的位置
  3. 这样的话, 整个数组最终就会变成有序的
#![allow(unused)]
fn main() {
/// 递归风格的插入排序算法
pub fn recursive_insertion_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    let len = arr.len();
    if len < 2 {
        return;
    }

    // 先将 list[..(len-1)] 中的元素排序.
    recursive_insertion_sort(&mut arr[..len - 1]);

    // 然后将最后一个元素插入到合适的位置.
    for i in (1..len).rev() {
        if arr[i - 1] > arr[i] {
            arr.swap(i - 1, i);
        } else {
            break;
        }
    }
}
}

二分插入排序法 Binary Insertion Sort

二分插入排序法结合了二分查找 (binary search) 与插入排序 (insertion sort).

根据上面的描述, 在对第 k 个元素进行排序时, list[0..k] 这部分已经是有序的了, 然后拿着第 k 个元素与它左侧的每个元素进行比较并交换, 直到找到合适的位置, 这个过程的时间复杂度是 O(k). 但因为 list[0..k] 数组已经是有序的了, 我们可以利用二分查找法 (binary search) 快速查找到第 k 个元素合适的位置, 这个过程的时间复杂度是 O(log k).

算法的实现如下所示:

#![allow(unused)]
fn main() {
fn binary_search<T>(arr: &[T], target: &T) -> usize
where
    T: PartialOrd,
{
    let mut left = 0;
    let mut right = arr.len() - 1;
    while left < right {
        let middle = left + (right - left) / 2;
        // 找到了相等的元素, 就返回该位置的下一个位置
        if arr[middle] == *target {
            return middle + 1;
        } else if arr[middle] < *target {
            left = middle + 1;
        } else {
            right = middle;
        }
    }

    // 没有找到相等的元素, 就返回期望的位置.
    if arr[arr.len() - 1] < *target {
        return arr.len();
    }
    if arr[0] > *target {
        return 0;
    }
    left
}

/// 二分插入排序法 binary insertion sort
pub fn binary_insertion_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    let len = arr.len();
    if len < 2 {
        return;
    }

    for i in 1..len {
        let target_pos = binary_search(&arr[..i], &arr[i]);
        for j in (target_pos..i).rev() {
            arr.swap(j, j + 1);
        }
    }
}
}

上述优化对选择排序的影响不大, 主要原因是耗时的操作在于移动数组中的元素, 而不是查找元素的合适位置.

插入排序的特点

  • 时间复杂度是 O(n^2), 空间复杂度是 O(1)
    • 如果传入的数据是增序排好的, 那么只需要 N-1 次的比较, 以及 0 次的交换
    • 如果传入的数据是降序排好的, 那么需要 N^2/2 次的比较, 以及 N^2/2 次的交换
    • 如果是乱序的, 大概需要 N^2/4 次的比较, 以及 N^2/4 次的交换
  • 插入排序是稳定排序 (stable sort)
  • 它是原地排序 in-place sort
  • 插入排序比较适合元素较少的数组
  • 插入排序适合基本已排序好的数组
  • 插入排序常用于组合排序算法中, 用于排序较少元素的部分数组; 比如 cpp 里面的 std::sort() 以及 python 里的 timsort

选择排序 Selection sort

选择排序的逻辑很简单, 将数组分成两部分:

  • 左侧部分是排序好的, 按顺序放着较小的元素
  • 右侧部分是未排序的, 放着较大的元素

选择排序的步骤

  1. 遍历数组, 找到最小的元素, 让它与最左侧元素交换
  2. 遍历数组中剩下的元素, 找到最小的元素, 让它与最左侧第二位元素交换
  3. 重复上面的步骤, 直到所有元素都有序排列

我们以 arr = [9, 4, 1, 7]; 为例进行演示:

首先找到最小的元素 1, 把它与最左侧元素相交换:

selection sort pass 1

第二阶段, 找到剩下元素中最小的元素 4, 把它与左侧第二位相交换:

selection sort pass 2

第三阶段, 找到最小的元素 7, 把它与左侧第三个元素相交换:

selection sort pass 3

到达了数组的最右侧, 所有元素都已排好序.

选择排序的代码实现

#![allow(unused)]
fn main() {
pub fn selection_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    let len = arr.len();
    if arr.len() < 2 {
        return;
    }
    for i in 0..(len - 1) {

        // 找到最小元素的索引
        let mut min_index = i;
        for j in (i + 1)..len {
            if arr[j] < arr[min_index] {
                min_index = j;
            }
        }

        // 如果最小元素不是 `list[i]`, 就交换两个元素
        if i != min_index {
            arr.swap(i, min_index);
        }
    }
}
}

递归实现选择排序

以上代码是用的迭代方式实现的选择排序, 接下来我们以递归的方式重新实现它.

#![allow(unused)]
fn main() {
/// 递归实现选择排序
pub fn recursive_selection_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    fn get_min_index<T>(list: &[T], i: usize, len: usize) -> usize
    where
        T: PartialOrd,
    {
        if i == len - 1 {
            return i;
        }
        let j = get_min_index(list, i + 1, len);
        if list[i] < list[j] {
            i
        } else {
            j
        }
    }

    let len = arr.len();
    if arr.len() < 2 {
        return;
    }

    let min_index = get_min_index(arr, 0, len);
    // 将最小的元素交换到最左侧
    if min_index != 0 {
        arr.swap(0, min_index);
    }

    // 递归排序剩下的元素
    recursive_selection_sort(&mut arr[1..]);
}
}

优化选择排序

默认实现的选择排序, 在每次循环时会找到最小的元素, 然后把它放在数组的左侧部分. 每次循环时, 我们可以同时找到最大的元素, 然后把它放在数组的右侧部分. 这样的话, 每个循环就可以同时找到最小和最大的元素.

#![allow(unused)]
fn main() {
/// 选择排序的一个小优化.
///
/// 将最小的元素放在左侧的同时, 将最大的元素放在右侧.
pub fn two_way_selection_sort<T>(arr: &mut [T])
where
    T: PartialOrd + std::fmt::Debug,
{
    let len = arr.len();
    if arr.len() < 2 {
        return;
    }

    let mut start = 0;
    let mut end = len - 1;
    while start < end {
        // 找到最小元素的索引
        let mut min_index = start;
        let mut max_index = start;
        for i in start..=end {
            if arr[i] < arr[min_index] {
                min_index = i;
            }
            if arr[i] > arr[max_index] {
                max_index = i;
            }
        }

        // 交换最小元素
        if start != min_index {
            arr.swap(start, min_index);
        }

        // 交换最大元素
        if end != max_index {
            if start == min_index {
                // 如果没有交换最小元素, 说明数组中的元素还没有移动过, 可以直接交换
                arr.swap(end, max_index);
            } else {
                // 这时, 最小元素已经移到了最左侧, 我们需要判断这个移位操作给最大值带来的影响.
                if max_index == start {
                    // 此时, 最大值已经被移到了 `list[min_index]`.
                    if end != min_index {
                        arr.swap(end, min_index);
                    }
                } else {
                    arr.swap(end, max_index);
                }
            }
        }

        start += 1;
        if end > 1 {
            end -= 1;
        }
    }
}
}

选择排序支持稳定排序

默认实现的选择排序算法, 是将最小元素交换到它的目标位置, 这样的话移动元素的次数很少, 但是是不稳定排序. 为了实现稳定排序, 我们可以插入排序的方式, 将最小元素插入到目标位置, 然后将其它元素向右移动一个位置, 尽管这样一来性能比较差.

#![allow(unused)]
fn main() {
/// 选择排序的一个小优化.
///
/// 将最小的元素放在左侧的同时, 将最大的元素放在右侧.
pub fn two_way_selection_sort<T>(arr: &mut [T])
where
    T: PartialOrd + std::fmt::Debug,
{
    let len = arr.len();
    if arr.len() < 2 {
        return;
    }

    let mut start = 0;
    let mut end = len - 1;
    while start < end {
        // 找到最小元素的索引
        let mut min_index = start;
        let mut max_index = start;
        for i in start..=end {
            if arr[i] < arr[min_index] {
                min_index = i;
            }
            if arr[i] > arr[max_index] {
                max_index = i;
            }
        }

        // 交换最小元素
        if start != min_index {
            arr.swap(start, min_index);
        }

        // 交换最大元素
        if end != max_index {
            if start == min_index {
                // 如果没有交换最小元素, 说明数组中的元素还没有移动过, 可以直接交换
                arr.swap(end, max_index);
            } else {
                // 这时, 最小元素已经移到了最左侧, 我们需要判断这个移位操作给最大值带来的影响.
                if max_index == start {
                    // 此时, 最大值已经被移到了 `list[min_index]`.
                    if end != min_index {
                        arr.swap(end, min_index);
                    }
                } else {
                    arr.swap(end, max_index);
                }
            }
        }

        start += 1;
        if end > 1 {
            end -= 1;
        }
    }
}
}

选择排序的特点

  1. 即使数组中的元素基本排序好, 也需要遍历所有元素并比较大小, 这种情况下效率较低, 依然需要 n^2 / 2 次比较操作 以及 n 次交换, 平均时间复杂度是 O(n log(n)), 空间复杂度是 O(1)
  2. 在所有排序算法中, 选择排序移动元素的次数最少, 每个元素最多只移动一次, 就可以移到最终位置; 这个算法比较适合那种比较元素时的成本低, 但移动元素成本比较高的情况 (比如, 移动文件中的内容)
  3. 选择排序是原地排序 (in-place sort)
  4. 选择排序是 in-adaptive sort
  5. 默认实现的选择排序算法是不稳定排序 (unstable), 但优化后的算法可以实现稳定排序 (stable sort)

归并排序 Merge sort

归并排序是 分治算法 的经典实现. 它将数组分成较小的数组并排序, 然后再将它们合并在一起, 得到的数组就是有序的了.

归并排序的步骤

默认实现的递归排序是自顶向下(top-down merge sort)的, 即将整个数组递归分隔.

  1. 分隔 divide: 将数组递归分成两部分子数组, 直到每部分只剩下一个元素为止
  2. 攻克 conquer: 使用分治算法排序每个子数组
  3. 合并 merge: 将排序好的子数组有序合并在一起

第一阶段: 将数组递归分隔 (partition) 成左右两部分:

merge sort partition pass

第二阶段, 将子数组合并在一起:

merge sort merge pass

归并排序的实现

#![allow(unused)]

fn main() {
/// 对于元素个数为 `N` 的数组, 自顶向下的归并排序 (top-down merge sort)
/// 最多使用 `N log(N)` 次比较以及 `6N log(N)` 次元素访问操作.
#[inline]
pub fn topdown_merge_sort<T>(arr: &mut [T])
where
    T: PartialOrd + Clone,
{
    if arr.is_empty() {
        return;
    }
    sort(arr, 0, arr.len() - 1);
}

/// 排序 `arr[low..=high]` 部分.
fn sort<T>(arr: &mut [T], low: usize, high: usize)
where
    T: PartialOrd + Clone,
{
    if low >= high {
        return;
    }

    let middle = low + (high - low) / 2;

    // 递归排序左侧部分数组
    sort(arr, low, middle);
    // 递归排序右侧部分数组
    sort(arr, middle + 1, high);

    // 合并左右两侧部分数组
    if arr[middle] > arr[middle + 1] {
        merge(arr, low, middle, high);
    }
}

/// 合并 `arr[low..=middle]` 以及 `arr[middle+1..=high]` 两个子数组.
///
/// 它不是原地合并.
#[allow(clippy::needless_range_loop)]
fn merge<T>(arr: &mut [T], low: usize, middle: usize, high: usize)
where
    T: PartialOrd + Clone,
{
    // 辅助数组, 先将数组复制一份.
    let aux = arr[low..=high].to_vec();

    // 再合并回原数组.
    let mut i = low;
    let mut j = middle + 1;

    for k in low..=high {
        if i > middle {
            arr[k] = aux[j - low].clone();
            j += 1;
        } else if j > high {
            arr[k] = aux[i - low].clone();
            i += 1;
        } else if aux[j - low] < aux[i - low] {
            arr[k] = aux[j - low].clone();
            j += 1;
        } else {
}

归并排序的特点

  • 归并排序的时间复杂度是 O(n log(n)), 空间复杂度是 O(N)

元素较少时, 使用插入排序

在排序阶段, 如果数组元素较少时仍然使用递归的归并排序的话, 并不划算, 因为会有大量的递归分支被调用, 还可能导致栈溢出. 为此我们设置一个常量, CUTOFF=24, 当数组元素个数小于它时, 直接使用插入排序.

另外, 我们还在递归调用之前, 创建了辅助数组 aux, 这样就可以在合并时重用这个数组, 以减少内存的分配.

#![allow(unused)]
fn main() {
            i += 1;
        }
    }
}

/// 对于元素数较少的数组, 使用插入排序
pub fn insertion_merge_sort<T>(arr: &mut [T])
where
    T: PartialOrd + Clone,
{
    if arr.is_empty() {
        return;
    }
    let cutoff: usize = 24;
    let mut aux = arr.to_vec();
    sort_cutoff_with_insertion(arr, 0, arr.len() - 1, cutoff, &mut aux);
}

/// 排序 `arr[low..=high]` 部分, 如果元数较少, 就使用插入排序.
fn sort_cutoff_with_insertion<T>(
    arr: &mut [T],
    low: usize,
    high: usize,
    cutoff: usize,
    aux: &mut Vec<T>,
) where
    T: PartialOrd + Clone,
{
    if low >= high {
        return;
    }

    if high - low <= cutoff {
        insertion_sort(&mut arr[low..=high]);
        return;
    }

    let middle = low + (high - low) / 2;

    // 递归排序左侧部分数组
    sort_cutoff_with_insertion(arr, low, middle, cutoff, aux);
    // 递归排序右侧部分数组
    sort_cutoff_with_insertion(arr, middle + 1, high, cutoff, aux);

    // 合并左右两侧部分数组
    if arr[middle] > arr[middle + 1] {
        merge_with_aux(arr, low, middle, high, aux);
    }
}

/// 合并 `arr[low..=middle]` 以及 `arr[middle+1..=high]` 两个子数组.
///
/// 它不是原地合并.
#[allow(clippy::needless_range_loop)]
fn merge_with_aux<T>(arr: &mut [T], low: usize, middle: usize, high: usize, aux: &mut [T])
where
    T: PartialOrd + Clone,
{
    // 辅助数组, 先将数组复制一份.
    for index in low..=high {
        aux[index].clone_from(&arr[index]);
    }

    // 再合并回原数组.
    let mut i = low;
    let mut j = middle + 1;

    for k in low..=high {
        if i > middle {
            arr[k] = aux[j].clone();
            j += 1;

/// 其思路是, 先将前 i 个元素调整为增序的, 随着 i 从 0 增大到 n, 整个序列就变得是增序了.
pub fn insertion_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    let len = arr.len();
    for i in 1..len {
        for j in (1..=i).rev() {
            if arr[j - 1] > arr[j] {
                arr.swap(j - 1, j);
            } else {
                break;
            }
        }
    }
}
}

元素较少时, 使用希尔排序

这个方法是基于以上方法, 用希尔排序来代替插入排序, 可以得到更好的性能. 而且 CUTOFF 值也可以更大一些. 经过几轮测试发现, 对于希尔排序来说, CUTOFF 的取值位于 [64..92] 之间时, 性能较好.

#![allow(unused)]
fn main() {
            arr[k] = aux[i].clone();
            i += 1;
        } else if aux[j] < aux[i] {
            arr[k] = aux[j].clone();
            j += 1;
        } else {
            arr[k] = aux[i].clone();
            i += 1;
        }
    }
}

/// 对于元素数较少的数组, 使用希尔排序
pub fn shell_merge_sort<T>(arr: &mut [T])
where
    T: PartialOrd + Clone,
{
    if arr.is_empty() {
        return;
    }

    let cutoff: usize = 72;
    let mut aux = arr.to_vec();
    sort_cutoff_with_shell(arr, 0, arr.len() - 1, cutoff, &mut aux);
}

/// 排序 `arr[low..=high]` 部分, 如果元数较少, 就使用希尔排序.
fn sort_cutoff_with_shell<T>(
    arr: &mut [T],
    low: usize,
    high: usize,
    cutoff: usize,
    aux: &mut Vec<T>,
) where
    T: PartialOrd + Clone,
{

    let middle = low + (high - low) / 2;

    // 递归排序左侧部分数组
    sort_cutoff_with_insertion(arr, low, middle, cutoff, aux);
    // 递归排序右侧部分数组
    sort_cutoff_with_insertion(arr, middle + 1, high, cutoff, aux);

    // 合并左右两侧部分数组
    if arr[middle] > arr[middle + 1] {
        merge_with_aux(arr, low, middle, high, aux);
    }
}

/// 合并 `arr[low..=middle]` 以及 `arr[middle+1..=high]` 两个子数组.
///
/// 它不是原地合并.
#[allow(clippy::needless_range_loop)]
fn merge_with_aux<T>(arr: &mut [T], low: usize, middle: usize, high: usize, aux: &mut [T])
where
    T: PartialOrd + Clone,
{
    // 辅助数组, 先将数组复制一份.
    for index in low..=high {
        aux[index].clone_from(&arr[index]);
    }

    // 再合并回原数组.
    let mut i = low;
    let mut j = middle + 1;

    for k in low..=high {
        if i > middle {
            arr[k] = aux[j].clone();
            j += 1;

/// Shell sort is a simple extension to insertion sort that allows exchanging
/// elements that far apart.
///
/// It produces partially sorted array (h-sorted array).
pub fn shell_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    const FACTOR: usize = 3;
    let len = arr.len();

    // 计算第一个 gap 的值, 大概是 len/3
    let mut h = 1;
    while h < len / FACTOR {
        h = FACTOR * h + 1;
    }

    while h >= 1 {
        // 使用插入排序, 将 `arr[0..h]` 排序好
        for i in h..len {
            let mut j = i;
            while j >= h && arr[j - h] > arr[j] {
                arr.swap(j - h, j);
                j -= h;
            }
        }

        h /= FACTOR;
    }
}
}

迭代形式实现的归并排序

迭代形式的归并排序, 又称为自下而上的归并排序 (bottom-up merge sort). 它的步骤如下:

  • 将连续的 2 个元素比较并合并在一起
  • 将连续的 4 个元素比较并合并在一起
  • 重复以上过程, 直到所有元素合并在一起

下面的流程图展示了一个简单的操作示例:

bottom-up merge sort

对应的代码实现如下:

#![allow(unused)]
fn main() {
    // 递归排序左侧部分数组
    sort_cutoff_with_shell(arr, low, middle, cutoff, aux);
    // 递归排序右侧部分数组
    sort_cutoff_with_shell(arr, middle + 1, high, cutoff, aux);

    // 合并左右两侧部分数组
    if arr[middle] > arr[middle + 1] {
        merge_with_aux(arr, low, middle, high, aux);
    }
}

/// 迭代形式的归并排序, 自底向上 bottom-up merge sort
pub fn bottom_up_merge_sort<T>(arr: &mut [T])
where
    T: PartialOrd + Clone,
{
    let len = arr.len();
    if len < 2 {
        return;
    }

    let mut aux = arr.to_vec();

    // 开始排序的数组大小, 从 1 到 len / 2
    // current_size 的取值是 1, 2, 4, 8, ...
    let mut current_size = 1;

    while current_size < len {
        // 归并排序的数组左侧索引
        let mut left_start = 0;

        // 子数组的起始点不同, 这样就可以遍历整个数组.
        // left_start 的取值是 0, 2 * current_size, 4 * current_size, ...
        // right_end 的取值是 2 * current_size, 4 * current_size, 6 * current_size, ...
        while left_start < len - 1 {
            let middle = (left_start + current_size - 1).min(len - 1);

    sort_cutoff_with_insertion(arr, middle + 1, high, cutoff, aux);

    // 合并左右两侧部分数组
    if arr[middle] > arr[middle + 1] {
        merge_with_aux(arr, low, middle, high, aux);
    }
}

/// 合并 `arr[low..=middle]` 以及 `arr[middle+1..=high]` 两个子数组.
///
/// 它不是原地合并.
#[allow(clippy::needless_range_loop)]
fn merge_with_aux<T>(arr: &mut [T], low: usize, middle: usize, high: usize, aux: &mut [T])
where
    T: PartialOrd + Clone,
{
    // 辅助数组, 先将数组复制一份.
    for index in low..=high {
        aux[index].clone_from(&arr[index]);
    }

    // 再合并回原数组.
    let mut i = low;
    let mut j = middle + 1;

    for k in low..=high {
        if i > middle {
            arr[k] = aux[j].clone();
            j += 1;
        } else if j > high {
            arr[k] = aux[i].clone();
            i += 1;
        } else if aux[j] < aux[i] {
            arr[k] = aux[j].clone();
}

三路归并排序 3-way merge sort

默认实现的归并排序, 是将数组分成左右两部分分别排序. 三路归并排序, 是将数组分成左中右三部分分别排序.

#![allow(unused)]
fn main() {
/// 三路归并排序
pub fn three_way_merge_sort<T>(arr: &mut [T])
where
    T: PartialOrd + Clone,
{
    if arr.is_empty() {
        return;
    }
    let mut aux = arr.to_vec();
    three_way_sort(arr, 0, arr.len() - 1, &mut aux);
}

/// 三路排序 `arr[low..=high]`
fn three_way_sort<T>(arr: &mut [T], low: usize, high: usize, aux: &mut Vec<T>)
where
    T: PartialOrd + Clone,
{
    // 如果数组长度小于2, 就返回.
    if low + 1 > high {
        return;
    }

    // 将数组分成三部分
    let middle1 = low + (high - low) / 3;
    let middle2 = low + 2 * ((high - low) / 3);

    // 递归排序各部分数组
    three_way_sort(arr, low, middle1, aux);
    three_way_sort(arr, middle1 + 1, middle2, aux);
    three_way_sort(arr, middle2 + 1, high, aux);

    // 合并三部分数组
    three_way_merge(arr, low, middle1, middle2, high, aux);
}

/// 合并 `arr[low..=middle1]`, `arr[middle1+1..=middle2]` 以及 `arr[middle2+1..=high]` 三个子数组.
///
/// 它不是原地合并.
#[allow(clippy::needless_range_loop)]
fn three_way_merge<T>(
    arr: &mut [T],
    low: usize,
    middle1: usize,
    middle2: usize,
    high: usize,
    aux: &mut [T],
) where
    T: PartialOrd + Clone,
{
    // 辅助数组, 先将数组复制一份.
    for index in low..=high {
        aux[index].clone_from(&arr[index]);
    }

    // 再合并回原数组.
    let mut i = low;
    let mut j = middle1 + 1;
    let mut k = middle2 + 1;
    let mut l = low;

    // 首先合并较小的子数组
    while i <= middle1 && j <= middle2 && k <= high {
        let curr_index = if aux[i] < aux[j] && aux[i] < aux[k] {
            &mut i
        } else if aux[j] < aux[k] {
            &mut j
        } else {
            &mut k
        };
        arr[l].clone_from(&aux[*curr_index]);
        *curr_index += 1;
        l += 1;
    }

    // 然后合并剩余部分的子数组
    while i <= middle1 && j <= middle2 {
        let curr_index = if aux[i] < aux[j] {
            &mut i
        } else {
            &mut j
        };
        arr[l].clone_from(&aux[*curr_index]);
        *curr_index += 1;
        l += 1;
    }

    while j <= middle2 && k <= high {
        let curr_index = if aux[j] < aux[k] {
            &mut j
        } else {
            &mut k
        };
        arr[l].clone_from(&aux[*curr_index]);
        *curr_index += 1;
        l += 1;
    }

    while i <= middle1 && k <= high {
        let curr_index = if aux[i] < aux[k] {
            &mut i
        } else {
            &mut k
        };
        arr[l].clone_from(&aux[*curr_index]);
        *curr_index += 1;
        l += 1;
    }

    while i <= middle1 {
        arr[l].clone_from(&aux[i]);
        i += 1;
        l += 1;
    }
    while j <= middle2 {
        arr[l].clone_from(&aux[j]);
        j += 1;
        l += 1;
    }
    while k <= high {
        arr[l].clone_from(&aux[k]);
        k += 1;
        l += 1;
    }
}
}

三路归并排序的特点:

  • 时间复杂度是 O(n log_3(n)), 空间复杂度是 O(n)
  • 但因为在 merge_xx() 函数中引入了更多的比较操作, 其性能可能更差

原地归并排序

原地归并排序, 是替代了辅助数组, 它使用类似插入排序的方式, 将后面较大的元素交换到前面合适的位置. 尽管省去了辅助数组, 但是因为移动元素的次数显著境多了, 其性能表现并不好.

下面的流程图展示了一个原地归并排序的示例:

in place merge sort merge pass

#![allow(unused)]
fn main() {
/// 原地归并排序
///
/// 尽管它不需要辅助数组, 但它的性能差得多, 时间复杂度是 `O(N^2 Log(N))`, 而默认实现的归并排序的
/// 时间复杂度是 `O(N Log(N))`.
pub fn in_place_merge_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    if arr.is_empty() {
        return;
    }
    sort_in_place(arr, 0, arr.len() - 1);
}

/// 原地排序 `arr[low..=high]`
fn sort_in_place<T>(arr: &mut [T], low: usize, high: usize)
where
    T: PartialOrd,
{
    if low >= high {
        return;
    }

    let middle = low + (high - low) / 2;
    sort_in_place(arr, low, middle);
    sort_in_place(arr, middle + 1, high);

    if arr[middle] > arr[middle + 1] {
        merge_in_place(arr, low, middle, high);
    }
}

/// 原地合并 `arr[low..=middle]` 以及 `arr[middle+1..=high]` 两个子数组.
fn merge_in_place<T>(arr: &mut [T], mut low: usize, mut middle: usize, high: usize)
where
    T: PartialOrd,
{
    let mut low2 = middle + 1;
    debug_assert!(arr[middle] > arr[low2]);

    while low <= middle && low2 <= high {
        if arr[low] <= arr[low2] {
            low += 1;
        } else {
            // 将所有元素右移, 并将 arr[low2] 插入到 arr[low] 所在位置. 这一步很慢.
            for index in (low..low2).rev() {
                arr.swap(index, index + 1);
            }

            // 更新所有的索引
            low += 1;
            middle += 1;
            low2 += 1;
        }
    }
}

}

原地归并排序的特点:

  • 时间复杂度度是 O(N^2 Log(N)), 空间复杂度是 O(1)
  • c++ 的标准库里有实现类似的算法, 参考 inplace_merge

优化原地归并排序

上面的原地归并排序, 每次只移动一个元素间隔. 类似于希尔排序, 我们可以增大移动元素的间隔 (gap), 来减少 移动元素的次数.

#![allow(unused)]
fn main() {
/// 对原地归并排序的优化
///
/// 它不需要辅助数组, 它参考了希尔排序, 通过调整元素间隔 gap 减少元素移动次数.
pub fn in_place_shell_merge_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    if arr.is_empty() {
        return;
    }
    sort_in_place_with_shell(arr, 0, arr.len() - 1);
}

/// 原地排序 `arr[low..=high]`
fn sort_in_place_with_shell<T>(arr: &mut [T], low: usize, high: usize)
where
    T: PartialOrd,
{
    if low >= high {
        return;
    }

    let middle = low + (high - low) / 2;
    sort_in_place_with_shell(arr, low, middle);
    sort_in_place_with_shell(arr, middle + 1, high);

    merge_in_place_with_shell(arr, low, high);
}

/// 使用希尔排序的方式原地合并 `arr[low..=middle]` 以及 `arr[middle+1..=high]` 两个子数组.
///
/// 时间复杂度 `O(N Log(N))`, 空间复杂度 `O(1)`
fn merge_in_place_with_shell<T>(arr: &mut [T], low: usize, high: usize)
where
    T: PartialOrd,
{
    #[must_use]
    #[inline]
    const fn next_gap(gap: usize) -> usize {
        const FACTOR: usize = 2;
        if gap == 1 {
            0
        } else {
            gap.div_ceil(FACTOR)
        }
    }
    let len = high - low + 1;
    let mut gap = next_gap(len);

    while gap > 0 {
        for i in low..=(high - gap) {
            let j = i + gap;
            // 每次间隔多个元素进行比较和交换.
            if arr[i] > arr[j] {
                arr.swap(i, j);
            }
        }
        gap = next_gap(gap);
    }
}
}
  • 时间复杂度度是 O(n log(n) log(n)), 空间复杂度是 O(1)

Timsort

Timsort 在 Python, Java 等编程语言的标准库中都有使用, 综合性能比较好.

Timsort 是对归并排序(merge sort)的优化.

Timsort 的步骤

它的优化思路是:

  • 先将数组分成相同间隔的子数组, 常用的间隔值是 32 或者 24
  • 然后用插入排序(或者考虑用希尔排序) 对这些子数组进行排序, 因为这些子数组比较短小, 插入排序的效率比较高
  • 排序后, 依次将子数组合并在一起形成有序的大数组, 直到整个数组变得有序
  • 合并子数组的方法与归并排序里一致, 不再详述
  • 如果数组中的元素较少, 就只会使用插入排序

下图展示了 timsort 的一个示例:

timsort

Timsort 的实现

#![allow(unused)]
fn main() {
use crate::insertion_sort::insertion_sort;
use crate::shell_sort::shell_sort;

/// Timsort 是对归并排序 (merge sort) 的优化.
pub fn timsort<T>(arr: &mut [T])
where
    T: PartialOrd + Clone,
{
    const RUN: usize = 32;

    let len = arr.len();
    if len < 2 {
        return;
    }

    // 先将数组分隔成大小相同的子数组, 并利用插入排序进行排序.
    // 插入排序比较善于处理已基本有序的较小的数组.
    for i in (0..len).step_by(RUN) {
        let end = (i + RUN).min(len);
        insertion_sort(&mut arr[i..end]);
    }

    // 然后将各个子数组合并在一起
    // 数组间隔依次是 RUN, RUN * 2, RUN * 4, ...
    let mut size = RUN;
    while size < len {
        // 合并子数组
        for left in (0..len).step_by(2 * size) {
            // 两个子数组分别是 `arr[left..=middle]` 和 `arr[middle+1..=right]`.
            let middle = left + size - 1;
            let right = (left + 2 * size - 1).min(len - 1);

            if middle < right {
                merge(arr, left, middle, right);
            }
        }

        size *= 2;
    }
}

/// 合并子数组 `arr[left..=middle]` 和 `arr[middle+1..=right]`
fn merge<T>(arr: &mut [T], left: usize, middle: usize, right: usize)
where
    T: PartialOrd + Clone,
{
    // 先创建辅助数组
    let aux_left = arr[left..=middle].to_vec();
    let aux_right = arr[middle + 1..=right].to_vec();
    let left_len = middle - left + 1;
    let right_len = right - middle;

    // 合并子数组
    let mut i = 0;
    let mut j = 0;
    let mut k = left;
    while i < left_len && j < right_len {
        if aux_left[i] < aux_right[j] {
            arr[k].clone_from(&aux_left[i]);
            i += 1;
        } else {
            arr[k].clone_from(&aux_right[j]);
            j += 1;
        }
        k += 1;
    }

    // 最后复制剩下的元素
    while i < left_len {
        arr[k].clone_from(&aux_left[i]);
        i += 1;
        k += 1;
    }

    while j < right_len {
        arr[k].clone_from(&aux_right[j]);
        j += 1;
        k += 1;
    }
}

/// 其思路是, 先将前 i 个元素调整为增序的, 随着 i 从 0 增大到 n, 整个序列就变得是增序了.
pub fn insertion_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    let len = arr.len();
    for i in 1..len {
        for j in (1..=i).rev() {
            if arr[j - 1] > arr[j] {
                arr.swap(j - 1, j);
            } else {
                break;
            }
        }
    }
}
}

使用希尔排序代替插入排序

上面提到了, 可以用希尔排序来代替插入排序, 可以将子数组的间隔设置得更大些, 我们选取 RUN = 64;

#![allow(unused)]
fn main() {
/// 使用希尔排序代替插入排序
///
/// 只创建一次辅助数组
pub fn shell_timsort<T>(arr: &mut [T])
where
    T: PartialOrd + Clone,
{
    const RUN: usize = 64;

    let len = arr.len();
    if len < 2 {
        return;
    }

    // 先将数组分隔成大小相同的子数组, 并利用插入排序进行排序.
    // 插入排序比较善于处理已基本有序的较小的数组.
    for i in (0..len).step_by(RUN) {
        let end = (i + RUN).min(len);
        shell_sort(&mut arr[i..end]);
    }

    // 然后将各个子数组合并在一起
    // 数组间隔依次是 RUN, RUN * 2, RUN * 4, ...
    let mut size = RUN;
    let mut aux = arr.to_vec();

    while size < len {
        // 合并子数组
        for left in (0..len).step_by(2 * size) {
            // 两个子数组分别是 `arr[left..=middle]` 和 `arr[middle+1..=right]`.
            let middle = left + size - 1;
            let right = (left + 2 * size - 1).min(len - 1);

            if middle < right {
                merge_with_aux(arr, left, middle, right, &mut aux);
            }
        }

        size *= 2;
    }
}

/// 合并子数组 `arr[left..=middle]` 和 `arr[middle+1..=right]`
fn merge_with_aux<T>(arr: &mut [T], left: usize, middle: usize, right: usize, aux: &mut [T])
where
    T: PartialOrd + Clone,
{
    // 先初始化辅助数组
    for i in left..=right {
        aux[i].clone_from(&arr[i]);
    }

    // 合并子数组
    let mut i = left;
    let mut j = middle + 1;
    let mut k = left;
    while i <= middle && j <= right {
        if aux[i] < aux[j] {
            arr[k].clone_from(&aux[i]);
            i += 1;
        } else {
            arr[k].clone_from(&aux[j]);
            j += 1;
        }
        k += 1;
    }

    while i <= middle {
        arr[k].clone_from(&aux[i]);
        i += 1;
        k += 1;
    }
    while j <= right {
        arr[k].clone_from(&aux[j]);
        j += 1;
        k += 1;
    }
}

#[cfg(test)]
mod tests {
    use crate::timsort::{shell_timsort, timsort};

    #[test]
    fn test_timsort() {

/// Shell sort is a simple extension to insertion sort that allows exchanging
/// elements that far apart.
///
/// It produces partially sorted array (h-sorted array).
pub fn shell_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    const FACTOR: usize = 3;
    let len = arr.len();

    // 计算第一个 gap 的值, 大概是 len/3
    let mut h = 1;
    while h < len / FACTOR {
        h = FACTOR * h + 1;
    }

    while h >= 1 {
        // 使用插入排序, 将 `arr[0..h]` 排序好
        for i in h..len {
            let mut j = i;
            while j >= h && arr[j - h] > arr[j] {
                arr.swap(j - h, j);
                j -= h;
            }
        }

        h /= FACTOR;
    }
}
}

Timsort 的特点

  • 最差情况下的时间复杂度是: O(n log(n)), 最间复杂度是 O(n)
  • 如果数组已基本有序, 最好情况下的时间复杂度是 O(n)
  • 是稳定排序, 不是原地排序 (in-place sort)
  • 与归并排序不同的是, 它不需要递归调用自身将数组分成左右子数组
  • timsort 与插入归并排序的区别较大

参考

快速排序 Quicksort

与归并排序类似, 快速排序也是分治算法 的经典实践.

选择基准值 pivot 的方法有多种, 比如:

  • 总是选择第一个元素
  • 总是选择最后一个元素
  • 从数组中随机选择一个元素
  • 选择数组中的中值 median

快速排序的步骤

快速排序的关键在于基准值 pivot 的选择.

  1. 我们选取数组的最后一个元素作为基准值 pivot, 分隔数组为左右两部分
    1. 使用变量 i 标记当前比基准值大的元素位置
    2. 遍历数组, 把比基准值小的元素交换到 i 的左侧, 比基准值大的元素留在元素 i 的右侧
    3. 最后, 把元素 i 与数组最右侧的基准值元素交换位置, 这样就把基准值放在了它的最终位置
  2. 将数组分成两部分, 左侧部分的元素值都比基准值小, 右侧部分比基准值大
  3. 然后递归调用快速排序算法, 对左右两侧子数组进行排序

下面以 arr = [1, 8, 3, 9, 4]; 为例子展示如何对数组分区.

首先选择最后一个元素 4 作为基准值 pivot. 将第一个元素 1 与基准值比较, 它比基准值小, 就需要交换元素 swap(i, j), 并将索引 i 右移一位:

quicksort partition pass1

将第二个元素 8 与基准值比较, 它比基准值大, 就什么都不做:

quicksort partition pass2

将第三个元素 3 与基准值比较, 它比基准值小, 就需要交换元素 swap(i, j), 并将索引 i 右移一位:

quicksort partition pass3

将第四个元素 9 与基准值比较, 它比基准值大, 就什么都不做:

quicksort partition pass4

最后一步, 将基准值 pivot 元素与当前的元素 i 进行交换, 这样的话 pivot 就被移动到了它的最终位置:

quicksort partition pass5

快速排序的实现

默认使用最后一个元素作为基准值 pivot. 如果是已排序好的数组, 这种算法是最差情况, 时间复杂度是 O(n^2).

#![allow(unused)]
fn main() {
/// 使用最后一个元素作为基准值 pivot
///
/// 如果是已排序好的数组, 这种算法是最差情况
#[inline]
pub fn quicksort<T: PartialOrd>(arr: &mut [T]) {
    if arr.len() < 2 {
        return;
    }
    tail_quicksort_helper(arr, 0, arr.len() - 1);
}

fn tail_quicksort_helper<T: PartialOrd>(arr: &mut [T], low: usize, high: usize) {
    if low >= high {
        return;
    }

    // 按照基数的位置, 将数组划分成左右两个子数组.
    let pivot_index = partition_pivot_at_right(arr, low, high);
    // 对左右两个子数组分别执行快速排序
    if pivot_index > low + 1 {
        tail_quicksort_helper(arr, low, pivot_index - 1);
    }
    if pivot_index + 1 < high {
        tail_quicksort_helper(arr, pivot_index + 1, high);
    }
}

// 选择最右侧的元素作为基准值
fn partition_pivot_at_right<T: PartialOrd>(arr: &mut [T], low: usize, high: usize) -> usize {
    let pivot_index = high;

    // 以 pivot 为基准, 把数组划分成三部分: 小于 pivot, pivot, 大于等于 pivot
    // i 用于标记比 pivot 大的元素
    let mut i = low;
    // j 用于遍历整个数组
    for j in low..high {
        if arr[j] < arr[pivot_index] {
            arr.swap(i, j);
            i += 1;
        }
    }

    // 最后把基准值 pivot 移到合适的位置.
    // 此时, 数组中元素的顺序满足以下条件: 小于 pivot, pivot, 大于等于 pivot
    arr.swap(i, pivot_index);
    // 返回的是 pivot 所在的位置
    i
}
}

快速排序的特点

  • 最好情况的时间复杂度是 O(n log(n)), 平均情况下的时间复杂度是 O(n log(n))
  • 最差情况的时间复杂度是 O(n^2), 因为选择的基准值 pivot 很不合适
  • 如果不考虑递归调用的栈空间, 快速排序的空间复要度是 O(1)
  • 如果考虑递归调用的栈空间, 最好情况下的空间复杂度是 O(log(n)), 最差情况下的空间复杂度是 O(n)
  • 不是稳定排序 (stable sort). 如果所需的排序算法不要求是稳定排序的, 那么我们应该优先考虑快速排序及其变体
  • 是原地排序 (in-place sort), 不需要辅助数组
  • 比归并排序 (merge sort) 要快, 不需要一个额外的数组来保存中间值
  • 它适对对大数据集做排序, 效率高; 不适合排序小的数据集
  • 快速排序是缓存友好型的 (cache-friendly), 能充分发挥缓存的局部性优势, 因为它是顺序遍历数组的

使用第一个元素作为基准值

上面我实现的分区算法, 使用最后一个元素作为基准值 pivot. 我们也可以选取数组的第一个元素作为基准值, 但如果数组已经是逆序排序的, 这种算法是最差情况, 时间复杂度是 O(n^2).

算法实现如下:

#![allow(unused)]
fn main() {
/// 总是选择第一个元素作为基准值
///
/// 果数组已经是逆序排序的, 这种算法是最差情况, 时间复杂度是 `O(n^2)`
#[inline]
pub fn head_quicksort<T: PartialOrd>(arr: &mut [T]) {
    if arr.len() < 2 {
        return;
    }
    head_quicksort_helper(arr, 0, arr.len() - 1);
}

fn head_quicksort_helper<T: PartialOrd>(arr: &mut [T], low: usize, high: usize) {
    if low >= high {
        return;
    }

    // 按照基数的位置, 将数组划分成左右两个子数组.
    let pivot_index = partition_pivot_at_left(arr, low, high);
    // 对左右两个子数组分别执行快速排序
    if pivot_index > low + 1 {
        head_quicksort_helper(arr, low, pivot_index - 1);
    }
    if pivot_index + 1 < high {
        head_quicksort_helper(arr, pivot_index + 1, high);
    }
}

/// 选择最左侧的元素作为基准值
fn partition_pivot_at_left<T: PartialOrd>(arr: &mut [T], low: usize, high: usize) -> usize {
    let pivot_index = low;

    // 以 pivot 为基准, 把数组划分成三部分: 小于等于 pivot, pivot, 大于 pivot
    // i 用于标记比 pivot 大的元素
    let mut i = high;
    // j 用于遍历整个数组
    for j in ((low + 1)..=high).rev() {
        if arr[j] > arr[pivot_index] {
            arr.swap(i, j);
            i -= 1;
        }
    }

    // 最后把基准值 pivot 移到合适的位置.
    // 此时, 数组中元素的顺序满足以下条件: 小于等于 pivot, pivot, 大于 pivot
    arr.swap(i, pivot_index);
    // 返回的是 pivot 所在的位置
    i
}
}

双指针风格的分区算法

上面的代码中, 我们都使用变量 j 来遍历数组, 这里我们也可以使用靠拢型双指针的写法遍历数组.

#![allow(unused)]
fn main() {
/// 总是选择第一个元素作为基准值, 并使用双指针法进行数组分区.
#[inline]
pub fn two_pointer_quicksort<T: PartialOrd>(arr: &mut [T]) {
    if arr.len() < 2 {
        return;
    }
    two_pointer_quicksort_helper(arr, 0, arr.len() - 1);
}

fn two_pointer_quicksort_helper<T: PartialOrd>(arr: &mut [T], low: usize, high: usize) {
    if low >= high {
        return;
    }

    // 按照基数的位置, 将数组划分成左右两个子数组.
    let pivot_index = partition_with_two_pointers(arr, low, high);
    // 对左右两个子数组分别执行快速排序
    if pivot_index > low + 1 {
        two_pointer_quicksort_helper(arr, low, pivot_index - 1);
    }
    if pivot_index + 1 < high {
        two_pointer_quicksort_helper(arr, pivot_index + 1, high);
    }
}

/// 使用双指针法选择最左侧的元素作为基准值
fn partition_with_two_pointers<T: PartialOrd>(arr: &mut [T], low: usize, high: usize) -> usize {
    let pivot_index = low;

    // 使用双指针法遍历数组, 以 pivot 为基准, 把数组划分成三部分:
    // 小于等于 pivot, pivot, 大于 pivot
    let mut left: usize = low;
    let mut right: usize = high;
    while left < right {
        // right 的位置左移, 直到 arr[right] 小于等于 pivot
        while left < right && arr[right] > arr[pivot_index] {
            right -= 1;
        }

        // left 的位置右移, 直到 arr[left] 大于 pivot
        while left < right && arr[left] <= arr[pivot_index] {
            left += 1;
        }

        // 交换元素
        arr.swap(left, right);
    }

    // 最后把基准值 pivot 移到合适的位置.
    // 此时, 数组中元素的顺序满足以下条件: 小于等于 pivot, pivot, 大于 pivot
    arr.swap(left, pivot_index);
    // 返回的是 pivot 所在的位置
    left
}
}

当元素较少时, 使用插入排序

当元素较少时, 递归调用快速排序算法会产生非常多的调用分支, 效率很低. 跟之前的优化方法类似, 当元素个数较少时, 我们直接调用插入排序.

#![allow(unused)]
fn main() {
/// 如果元素较少, 就使用插入排序
#[inline]
pub fn insertion_quicksort<T: PartialOrd>(arr: &mut [T]) {
    if arr.len() < 2 {
        return;
    }
    insertion_quicksort_helper(arr, 0, arr.len() - 1);
}

fn insertion_quicksort_helper<T: PartialOrd>(arr: &mut [T], low: usize, high: usize) {
    const CUTOFF: usize = 24;

    if low >= high {
        return;
    }

    // 数组中的元数个数低于一个阈值时, 使用插入排序
    if high - low + 1 < CUTOFF {
        insertion_sort(&mut arr[low..=high]);
        return;
    }

    // 按照基数的位置, 将数组划分成左右两个子数组.
    let pivot_index = partition_pivot_at_right(arr, low, high);
    // 对左右两个子数组分别执行快速排序
    if pivot_index > low + 1 {
        insertion_quicksort_helper(arr, low, pivot_index - 1);
    }
    if pivot_index + 1 < high {
        insertion_quicksort_helper(arr, pivot_index + 1, high);
    }
}

// 选择最右侧的元素作为基准值
fn partition_pivot_at_right<T: PartialOrd>(arr: &mut [T], low: usize, high: usize) -> usize {
    let pivot_index = high;

    // 以 pivot 为基准, 把数组划分成三部分: 小于 pivot, pivot, 大于等于 pivot
    // i 用于标记比 pivot 大的元素
    let mut i = low;
    // j 用于遍历整个数组
    for j in low..high {
        if arr[j] < arr[pivot_index] {
            arr.swap(i, j);
            i += 1;
        }
    }

    // 最后把基准值 pivot 移到合适的位置.
    // 此时, 数组中元素的顺序满足以下条件: 小于 pivot, pivot, 大于等于 pivot
    arr.swap(i, pivot_index);
    // 返回的是 pivot 所在的位置
    i
}

/// 其思路是, 先将前 i 个元素调整为增序的, 随着 i 从 0 增大到 n, 整个序列就变得是增序了.
pub fn insertion_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    let len = arr.len();
    for i in 1..len {
        for j in (1..=i).rev() {
            if arr[j - 1] > arr[j] {
                arr.swap(j - 1, j);
            } else {
                break;
            }
        }
    }
}
}

迭代形式的快速排序

默认情况下实现的快速排序使用了递归形式, 它用了尾递归调用来保存数组的左右边界值. 我们也可以显式地使用一个栈结构来手动保存它们, 就可以将快速排序改写成迭代形式:

#![allow(unused)]
fn main() {
/// 迭代形式的快速排序
///
/// 空间复杂度是 `O(n)`
#[inline]
pub fn iterative_quicksort<T: PartialOrd>(arr: &mut [T]) {
    if arr.len() < 2 {
        return;
    }
    iterative_quicksort_helper(arr, 0, arr.len() - 1);
}

fn iterative_quicksort_helper<T: PartialOrd>(arr: &mut [T], low: usize, high: usize) {
    if low >= high {
        return;
    }

    let len = high - low + 1;
    let mut stack = vec![0; len];

    // 入栈顺序是 (low, high)
    stack.push(low);
    stack.push(high);

    // 出栈顺序是 (high, low)
    while let (Some(high), Some(low)) = (stack.pop(), stack.pop()) {
        // 按照基数的位置, 将数组划分成左右两个子数组.
        let pivot_index = partition_pivot_at_right(arr, low, high);
        // 对左右两个子数组分别执行快速排序
        // 如果左侧子数组还有元素, 就入栈
        if pivot_index > low + 1 {
            stack.push(low);
            stack.push(pivot_index - 1);
        }
        // 如果 pivot 的右侧还有元素, 就入栈
        if pivot_index + 1 < high {
            stack.push(pivot_index + 1);
            stack.push(high);
        }
    }
}

// 选择最右侧的元素作为基准值
fn partition_pivot_at_right<T: PartialOrd>(arr: &mut [T], low: usize, high: usize) -> usize {
    let pivot_index = high;

    // 以 pivot 为基准, 把数组划分成三部分: 小于 pivot, pivot, 大于等于 pivot
    // i 用于标记比 pivot 大的元素
    let mut i = low;
    // j 用于遍历整个数组
    for j in low..high {
        if arr[j] < arr[pivot_index] {
            arr.swap(i, j);
            i += 1;
        }
    }

    // 最后把基准值 pivot 移到合适的位置.
    // 此时, 数组中元素的顺序满足以下条件: 小于 pivot, pivot, 大于等于 pivot
    arr.swap(i, pivot_index);
    // 返回的是 pivot 所在的位置
    i
}

/// 其思路是, 先将前 i 个元素调整为增序的, 随着 i 从 0 增大到 n, 整个序列就变得是增序了.
pub fn insertion_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    let len = arr.len();
    for i in 1..len {
        for j in (1..=i).rev() {
            if arr[j - 1] > arr[j] {
                arr.swap(j - 1, j);
            } else {
                break;
            }
        }
    }
}
}

随机选择一个元素作为基准值 pivot

尾递归优化 Tail call optimization

稳定快速排序 Stable Quicksort

双轴快速排序 Dual pivot Quicksort

三路快速排序 3-way Quicksort

参考

堆排序 Heap Sort

IntroSort

参考

pdqsort

Pattern-defeating quicksort 简称为 pdqsort.

参考

希尔排序 Shell Sort

接下面的几节介绍几个不常用的排序算法.

本节介绍的希尔排序 (shell sort) 是插入排序的 (insertion sort) 的变体.

插入排序的一个问题是, 将元素 k 移动到左侧排序好的数组中的位置时, 通常还要移动元素 k 左侧的元素, 而移动元素的成本比较高. 希尔排序对这个过程做了优化, 以减少移动元素的次数.

希尔排序将数组拆解成由 h 个元素组成的小数组, 依次降低h间隔的值, 直到其为1, 这样就减少了元素交换的次数.

希尔排序的步骤

  1. 初始化间隔值 h = len / 3
  2. 使用插入排序法, 将 arr[h..]arr[..h] 间的元素进行排序, 使用插入排序法, 但两个待比较的元素的间隔是 h, 而不是默认的 1, 这一步很重要, 它有助于减少元素的移动次数
  3. 减少间隔值, h /= 3, 重复上面的步骤, 直到最后一个循环 h = 1

这里的 h 值是由大到小变化的, 就是说, 每次移动的步长是h, 就是为了减少元素被移动的次数. 当 h = 1 时, 整个序列就完成排序了.

希尔排序的实现

#![allow(unused)]
fn main() {
/// Shell sort is a simple extension to insertion sort that allows exchanging
/// elements that far apart.
///
/// It produces partially sorted array (h-sorted array).
pub fn shell_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    const FACTOR: usize = 3;
    let len = arr.len();

    // 计算第一个 gap 的值, 大概是 len/3
    let mut h = 1;
    while h < len / FACTOR {
        h = FACTOR * h + 1;
    }

    while h >= 1 {
        // 使用插入排序, 将 `arr[0..h]` 排序好
        for i in h..len {
            let mut j = i;
            while j >= h && arr[j - h] > arr[j] {
                arr.swap(j - h, j);
                j -= h;
            }
        }

        h /= FACTOR;
    }
}
}

希尔排序的特点

  • 最差情况下的时间复杂度 O(n^2), 空间复杂度是 O(1)
  • 最好情竞下的时间复杂度是 Ω(n log(n))
  • 比插入排序快
  • 与插入排序不同的时, 希尔排序适合大中型的数组, 对于任意顺序的数组也有效

侏儒排序 Gnome Sort

侏儒排序又称为愚人排序 (Stupid Sort), 它类似于插入排序, 在移动元素时用到了的方法类似于冒泡排序, 它不需要使用多层循环嵌套.

侏儒排序的步骤

侏儒排序将数组分成两部分, 左侧部分是有序的, 右侧部分是无序的. 它只需要一层循环, 用于遍历数组中的所有元素. 将目标元素 k 与左侧的有序数组进行比较, 如果它更小, 就与左侧的元素交换位置, 并将循环体中的索引值向左移. 这样的话下次进入循环体时, 仍然访问的是元素 k, 然后重复上面的比较操作和交换操作, 直到元素 k 被放置在了 合适的位置.

第一阶段, 找到第二个元素 4, 将它与第一个元素进行比较并交换位置:

gnome sort pass 1

第二阶段, 找到第三个元素1, 将它与左侧的元素进行比较并换换位置:

gnome sort pass 2

第三阶段, 找到第三个元素7, 将它与左侧的元素进行比较并换换位置:

gnome sort pass 3

侏儒排序的实现

#![allow(unused)]
fn main() {
/// Gnome sort is a variation of the insertion sort sorting algorithm
/// that does not use nested loops.
///
/// [Gnome sort](https://en.wikipedia.org/wiki/Gnome_sort)
pub fn gnome_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    let mut index = 0;
    while index < arr.len() {
        // 当前元素比左侧元素大, 是有序的
        if index == 0 || arr[index] >= arr[index - 1] {
            index += 1;
        } else {
            // 当前元素比左侧元素小, 交换它们
            arr.swap(index, index - 1);
            index -= 1;
        }
    }
}
}

侏儒排序的特点

  • 它的时间复杂度是 O(n^2), 空间复杂度是 O(1)
  • 对于排序好的数组来说, 时间复杂度是 O(n)

桶排序 Bucket Sort

前文介绍的几种排序算法都是基于比较元素之间的关系 (comparison based), 这对于像字符串或者其它自定义数据类型也是有效的, 只需要实现 PartialOrd 即可, 具有通用性.

桶排序是基于元素的数值大小, 而不是比较关系 (non-comparison based), 这类算法只适合整数和定长的字符串.

桶排序也是一种线性排序方法. 它将元素分配到多个桶中, 然后对每个桶单独进行排序.

桶排序的步骤

  1. 根据原数组中元素的数值范围, 将数组分成 m 个桶, 每个桶将存放一定数值区间的元素, 而且这些数值区间有序不重叠
  2. 按顺序遍历数组, 将元素按数值大小放到目标桶中, 每个桶会存放相近或者相同的元素
  3. 使用插入排序等算法对每个桶排序
  4. 按照桶的顺序, 将每个桶中的元素依次存储到原数组

bucket sort

桶排序的实现

#![allow(unused)]
fn main() {
use crate::shell_sort::shell_sort;

/// 桶排序, 使用插入排序来处理每个桶.
#[allow(clippy::cast_sign_loss)]
pub fn bucket_sort(arr: &mut [i32]) {
    if arr.is_empty() {
        return;
    }

    // 对于插入排序来说, 元素的个数在这个范围内的效率比较高.
    let bucket_elements: usize = 72;
    let min_num: i32 = arr.iter().min().copied().unwrap_or_default();
    let max_num: i32 = arr.iter().max().copied().unwrap_or_default();
    // 计算数值范围.
    let range: i32 = max_num - min_num;
    // 计算桶的个数, 我们假设元素的数值是均匀分布的.
    // 这样的话就可以确定每个桶要存储的数值范围.
    // 尽可能把数值相近的元素放在一起.
    let bucket_count: usize = range as usize / bucket_elements + 1;
    // 创建一系列的桶.
    let mut buckets: Vec<Vec<i32>> = vec![vec![]; bucket_count];

    // 遍历数组, 将元素分配到每个桶中.
    // 这里是按数组的原有顺序插入到桶中的, 有相同数值的元素也会依照原先的顺序放置到同一个桶.
    for &num in arr.iter() {
        // 计算这个元素值处于哪个数值段, 并确定该放到哪个桶.
        let bucket_index: usize = (num - min_num) as usize / bucket_elements;
        buckets[bucket_index].push(num);
    }

    // 对每一个桶单独排序, 按照假设, 每个桶中的元素个数都比较少,
    // 使用插入排序可以发挥它的优势.
    // 并且插入排序是稳定排序, 所以该桶排序算法也是稳定排序.
    let mut index: usize = 0;
    for mut bucket in buckets {
        insertion_sort(&mut bucket);
        // 将这个桶中的元素合并到原先的数组中.
        arr[index..(index + bucket.len())].copy_from_slice(&bucket);
        index += bucket.len();

/// 其思路是, 先将前 i 个元素调整为增序的, 随着 i 从 0 增大到 n, 整个序列就变得是增序了.
pub fn insertion_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    let len = arr.len();
    for i in 1..len {
        for j in (1..=i).rev() {
            if arr[j - 1] > arr[j] {
                arr.swap(j - 1, j);
            } else {
                break;
            }
        }
    }
}
}

桶排序的特点

  • 如果给每个桶做排序是的算法是稳定排序的, 那么桶排序算法就是稳定排序
  • 时间复杂度是 O(n), 空间复杂度是 O(n + m)
  • 比快速排序还要快

使用希尔排序

上面的代码中, 我们使用插入排序来给每个桶排序, 这次我们换成希尔排序. 后者可以支持排序更多的元素, 依然保持较好的性能.

#![allow(unused)]
fn main() {
}

/// 桶排序的另一种实现, 使用希尔排序来处理每个桶.
#[allow(clippy::cast_sign_loss)]
pub fn shell_bucket_sort(arr: &mut [i32]) {
    if arr.is_empty() {
        return;
    }

    // 对于希尔排序来说, 元素的个数在这个范围内的效率比较高.
    let bucket_elements: usize = 72 * 2;

    let min_num: i32 = arr.iter().min().copied().unwrap_or_default();
    let max_num: i32 = arr.iter().max().copied().unwrap_or_default();
    let range: i32 = max_num - min_num;
    let bucket_count: usize = range as usize / bucket_elements + 1;
    let mut buckets: Vec<Vec<i32>> = vec![vec![]; bucket_count];

    for &num in arr.iter() {
        let bucket_index: usize = (num - min_num) as usize / bucket_elements;
        buckets[bucket_index].push(num);
    }
    let mut index: usize = 0;
    for mut bucket in buckets {
        shell_sort(&mut bucket);
        arr[index..(index + bucket.len())].copy_from_slice(&bucket);
        index += bucket.len();

/// Shell sort is a simple extension to insertion sort that allows exchanging
/// elements that far apart.
///
/// It produces partially sorted array (h-sorted array).
pub fn shell_sort<T>(arr: &mut [T])
where
    T: PartialOrd,
{
    const FACTOR: usize = 3;
    let len = arr.len();

    // 计算第一个 gap 的值, 大概是 len/3
    let mut h = 1;
    while h < len / FACTOR {
        h = FACTOR * h + 1;
    }

    while h >= 1 {
        // 使用插入排序, 将 `arr[0..h]` 排序好
        for i in h..len {
            let mut j = i;
            while j >= h && arr[j - h] > arr[j] {
                arr.swap(j - h, j);
                j -= h;
            }
        }

        h /= FACTOR;
    }
}
}

基数排序 Radix Sort

基数排序是基于数值的每一个整数位或字符串中的每个字符来排序, 直到整个数组变得有序.

基数排序基于上文介绍的桶排序的思想.

根据排序的方向可以划分为最低位基数排序 (Least Significant Digit, LSD) 和是高位基数排序 (Most Significant Digit, MSD).

基数排序的步骤

基数排序的实现

#![allow(unused)]
fn main() {
#[allow(clippy::cast_possible_truncation)]
pub fn radix_sort(arr: &mut [u32]) {
    const fn num_digits(mut num: u32) -> usize {
        let mut count: usize = 0;
        while num != 0 {
            count += 1;
            num /= 10;
        }
        count
    }

    if arr.is_empty() {
        return;
    }
    // 获取最大的位数
    let max_digits: usize = arr
        .iter()
        .map(|num| num_digits(*num))
        .max()
        .unwrap_or_default();

    for i in 0..max_digits {
        // bucket 长度为10, 代表了数字 0~9.
        let mut buckets = vec![vec![]; 10];

        for num in arr.iter() {
            // 这个 index 是关键, 它是每个元素在当前位上的数字
            let index: u32 = *num / 10_u32.pow(i as u32) % 10;
            buckets[index as usize].push(*num);
        }

        let mut index = 0;
        for bucket in buckets {
            for num in bucket {
                // 取出对应的元素, 更新到原始数组中
                arr[index] = num;
                index += 1;
            }
        }
    }
}
}

基数排序的特点

  • 基数排序是一种线性排序算法
  • 时间复杂度是 O(n * m), 空间复杂度是 O(n + m), 其中 n 是数组中的个数, m 是元数的最大位数
  • 可以对数值或者字符串排序
  • 基数排序是稳定排序

计数排序 Counting Sort

计数排序不是基于比较值的排序算法.

计数排序的步骤

计数排序的实现分几个阶段:

  • 首先遍历输入数组, 计算元素的取值范围
  • 生成计数数组, 其元素个数基于元素的取值范围确定
  • 遍历输入数组, 根据每个元素与最小元素的差值作为索引, 更新计数数组
  • 更新计数数组, 使之成为前缀和数组
  • 初始化输出数组
  • 从最后一个元素开始遍历输入数组, 每个元素都存放 num
    • 计算当前元素与最小元素的差值 delta_index
    • 从计数数组中取得该元素的索引值 let num_index = count_arr[delta_index]
    • 更新输出数组, 将 num 放到相应的位置, arr[num_index - 1] = num
    • 并更新计数数组, 将里面的索引值减1, count_arr[delta_index] -= 1

计数排序的实现

下面的算法限制了输入元素是 i32:

#![allow(unused)]
fn main() {
}

#[allow(clippy::cast_sign_loss)]
pub fn counting_sort(arr: &mut [i32]) {
    if arr.is_empty() {
        return;
    }
    let min_num: i32 = arr.iter().min().copied().unwrap_or_default();
    let max_num: i32 = arr.iter().max().copied().unwrap_or_default();
    // 计算数值范围
    let range: i32 = max_num - min_num;
    let size: usize = range as usize + 1;

    // 构造计数数组
    let mut count_arr = vec![0_usize; size];

    // 遍历输入数组, 更新计数数组
    for &num in arr.iter() {
        let delta: i32 = num - min_num;
        let index: usize = delta as usize;
        count_arr[index] += 1;
    }

    // 生成累积数组, prefix sum array
    for i in 1..size {
        count_arr[i] += count_arr[i - 1];
    }

    // 构造输入数组, 只读的
    let input_arr: Vec<i32> = arr.to_vec();

    // 从输入数组的右侧向左侧遍历, 这样实现的是稳定排序.
    for &num in input_arr.iter().rev() {
        // 计算当前值与最小值的差.
        let delta: i32 = num - min_num;
        let delta_index = delta as usize;
        // 从 count_arr 里取出该数值的相对位置
        let num_index: usize = count_arr[delta_index];
        // 把 num 放在对应的位置
        arr[num_index - 1] = num;

        // 同时更新 count_arr, 使之计数减1, 这样的话下一个相同数值的元素的索引值就被左移了一位.
        count_arr[delta_index] -= 1;
}

下面的代码对计数排序加入了泛型的支持, 注意它的类型 T 有很多限制:

#![allow(unused)]
fn main() {
use std::collections::BTreeMap;
use std::ops::Sub;

pub fn counting_sort_generic<T>(arr: &mut [T])
where
    T: Copy + Default + Ord + Sub<Output=T> + TryInto<usize>,
{
    if arr.is_empty() {
        return;
    }
    let min_num: T = arr.iter().min().copied().unwrap_or_default();
    let max_num: T = arr.iter().max().copied().unwrap_or_default();
    // 计算数值范围
    let range: T = max_num - min_num;
    let size: usize = range.try_into().unwrap_or_default() + 1;

    // 构造计数数组
    let mut count_arr = vec![0_usize; size];

    // 遍历数组, 更新计数数组
    for num in arr.iter() {
        let delta: T = *num - min_num;
        let index: usize = delta.try_into().unwrap_or_default();
        count_arr[index] += 1;
    }

    // 生成累积数组, prefix sum array
    for i in 1..size {
        count_arr[i] += count_arr[i - 1];
    }

    // 构造输入数组, 只读的
    let input_arr = arr.to_vec();

    for &num in input_arr.iter().rev() {
        let delta: T = num - min_num;
        let delta_index: usize = delta.try_into().unwrap_or_default();
        // 从 count_arr 里取出该数值的相对位置
        let num_index: usize = count_arr[delta_index];
        // 把 num 放在对应的位置
        arr[num_index - 1] = num;

        // 同时更新 count_arr, 使之计数减1, 这样的话下一个相同数值的元素的索引值就被左移了一位.
        count_arr[delta_index] -= 1;
    }
}

计数排序的特点

  • 空间复杂度是 O(n + m), n 是输入数组的大小, m 是计数数组的大小, 也就是元素的数值范围
  • 时间复杂度是 O(n + m)
  • 计数排序是稳定排序, 但不是原地排序 (in-place sorting)
  • 如果数组中的元素值所处的范围比较大的话, 计数排序的效率就比较低
  • 它需要较多的额外空间来存储中间值
  • 计数排序要比归并排序和快速排序等基于比较元素值的排序算法都要快
  • 计数排序不惧怕有重复的元素, 但是如果元素的取值范围比较大的话, 其效率就很低

使用 map 作为计数数组的容器

上面实现的计数排序, 其计数数组对于元素的取值范围很敏感, 甚至计数数组中可能有很多的值都是0, 它们 都被浪费掉了.

对此, 我们可以做一些优化, 使用 map 来存储计数数组中的值.

#![allow(unused)]
fn main() {
use std::collections::BTreeMap;
use std::ops::Sub;

pub fn counting_sort_generic<T>(arr: &mut [T])
where
    T: Copy + Default + Ord + Sub<Output=T> + TryInto<usize>,
{
    if arr.is_empty() {
        return;
    }
    let min_num: T = arr.iter().min().copied().unwrap_or_default();
    let max_num: T = arr.iter().max().copied().unwrap_or_default();
    // 计算数值范围
    let range: T = max_num - min_num;
    let size: usize = range.try_into().unwrap_or_default() + 1;

    // 构造计数数组
    let mut count_arr = vec![0_usize; size];

    // 遍历数组, 更新计数数组
    for num in arr.iter() {
        let delta: T = *num - min_num;
        let index: usize = delta.try_into().unwrap_or_default();
        count_arr[index] += 1;
    }

    // 生成累积数组, prefix sum array
    for i in 1..size {
        count_arr[i] += count_arr[i - 1];
    }

    // 构造输入数组, 只读的
    let input_arr = arr.to_vec();

    for &num in input_arr.iter().rev() {
        let delta: T = num - min_num;
        let delta_index: usize = delta.try_into().unwrap_or_default();
        // 从 count_arr 里取出该数值的相对位置
        let num_index: usize = count_arr[delta_index];
        // 把 num 放在对应的位置
        arr[num_index - 1] = num;

        // 同时更新 count_arr, 使之计数减1, 这样的话下一个相同数值的元素的索引值就被左移了一位.
        count_arr[delta_index] -= 1;
    }
}

#[allow(clippy::cast_sign_loss)]
pub fn counting_sort(arr: &mut [i32]) {
    if arr.is_empty() {
        return;
    }
    let min_num: i32 = arr.iter().min().copied().unwrap_or_default();
    let max_num: i32 = arr.iter().max().copied().unwrap_or_default();
    // 计算数值范围
    let range: i32 = max_num - min_num;
    let size: usize = range as usize + 1;

    // 构造计数数组
    let mut count_arr = vec![0_usize; size];

    // 遍历输入数组, 更新计数数组
    for &num in arr.iter() {
        let delta: i32 = num - min_num;
        let index: usize = delta as usize;
        count_arr[index] += 1;
    }

    // 生成累积数组, prefix sum array
    for i in 1..size {
        count_arr[i] += count_arr[i - 1];
    }

    // 构造输入数组, 只读的
    let input_arr: Vec<i32> = arr.to_vec();

    // 从输入数组的右侧向左侧遍历, 这样实现的是稳定排序.
    for &num in input_arr.iter().rev() {
        // 计算当前值与最小值的差.
        let delta: i32 = num - min_num;
        let delta_index = delta as usize;
        // 从 count_arr 里取出该数值的相对位置
        let num_index: usize = count_arr[delta_index];
        // 把 num 放在对应的位置
        arr[num_index - 1] = num;

        // 同时更新 count_arr, 使之计数减1, 这样的话下一个相同数值的元素的索引值就被左移了一位.
        count_arr[delta_index] -= 1;
    }
}

#[allow(clippy::cast_sign_loss)]
pub fn counting_sort_with_map(arr: &mut [i32]) {
    if arr.is_empty() {
        return;
    }

    // 构造字典, 存储元素的频率
    let mut freq_map: BTreeMap<i32, usize> = BTreeMap::new();
    // 遍历输入数组, 更新计数数组
    for &num in arr.iter() {
        *freq_map.entry(num).or_default() += 1;
    }

    // 遍历字典
    let mut i = 0;
    for (num, freq) in freq_map {
        for _j in 0..freq {
            arr[i] = num;
            i += 1;
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{counting_sort, counting_sort_generic, counting_sort_with_map};

    #[test]
    fn test_counting_sort() {
        let mut list = [0, 5, 3, 2, 2];
        counting_sort(&mut list);
        assert_eq!(list, [0, 2, 2, 3, 5]);

        let mut list = [-2, -5, -45];
        counting_sort(&mut list);
        assert_eq!(list, [-45, -5, -2]);

        let mut list = [
            -998_166, -996_360, -995_703, -995_238, -995_066, -994_740, -992_987, -983_833,
            -987_905, -980_069, -977_640,
        ];
        counting_sort(&mut list);
        assert_eq!(
            list,
            [
                -998_166, -996_360, -995_703, -995_238, -995_066, -994_740, -992_987, -987_905,
                -983_833, -980_069, -977_640,
            ]
        );
    }

    #[test]
    fn test_counting_sort_generic() {
        let mut list = [0, 5, 3, 2, 2];
        counting_sort_generic(&mut list);
        assert_eq!(list, [0, 2, 2, 3, 5]);

        let mut list = [-2, -5, -45];
        counting_sort_generic(&mut list);
        assert_eq!(list, [-45, -5, -2]);

        let mut list = [
            -998_166, -996_360, -995_703, -995_238, -995_066, -994_740, -992_987, -983_833,
            -987_905, -980_069, -977_640,
        ];
        counting_sort_generic(&mut list);
        assert_eq!(
            list,
            [
                -998_166, -996_360, -995_703, -995_238, -995_066, -994_740, -992_987, -987_905,
                -983_833, -980_069, -977_640,
            ]
        );
    }

    #[test]
    fn test_counting_sort_with_map() {
        let mut list = [0, 5, 3, 2, 2];
        counting_sort_with_map(&mut list);
        assert_eq!(list, [0, 2, 2, 3, 5]);

        let mut list = [-2, -5, -45];
        counting_sort_with_map(&mut list);
        assert_eq!(list, [-45, -5, -2]);

        let mut list = [
            -998_166, -996_360, -995_703, -995_238, -995_066, -994_740, -992_987, -983_833,
            -987_905, -980_069, -977_640,
        ];
        counting_sort_with_map(&mut list);
        assert_eq!(
            list,
            [
                -998_166, -996_360, -995_703, -995_238, -995_066, -994_740, -992_987, -987_905,
                -983_833, -980_069, -977_640,
            ]
        );
    }
}
}

该算法的特点是

  • 时间复杂度是 O(n log(n)), 空间复杂度是 O(n)
  • 即使输入数组的取值范围较大, 也不成问题

标准库中排序算法的实现

链表排序 List Sort

上一章介绍了数组的多种排序方法. 与数组不同的是, 链表结构不支持随机索引.

对链表中的元素进行排序, 有它自己的特点.

冒泡排序 Bubble Sort

插入排序 Insertion Sort

选择排序 Selection Sort

归并排序 Merge Sort

快速排序 Quicksort

外部排序 External Sorting

Multiway Merge

Polyphase Merge

Distribution Sort

Cache-oblivious Distribution Sort

查找 Searching

查找 (searching) 是数组最常用的操作之一. 所谓的查找操作, 就是在一组元素中找到某个特定的元素.

常用的查找算法有:

  • 线性查找 linear search
  • 二分查找 binary search
  • 三元查找/三叉查找 ternary search

在无序数组中查找

使用二分查找法进行有序数组的查找

线性查找 Linear Search

所谓的线性查找, 指的是从数组的一端开始, 依次遍历每一个元素, 找到目标元素后终止, 或者到达了数组的另一端才终止. 该算法还有一个别名, 叫顺序查找 sequential search.

线性查找的步骤

  1. 从数组的第一个元素开始遍历整个数组
  2. 将当前元素与目标元素进行比较
  3. 如果当前元素与相等, 就终止循环并返回当前元素的索引值
  4. 如果不相等, 就移到数组中的下一个元素
  5. 重复第2-4步, 直到数组的尾部
  6. 如果到达数组尾部后, 仍然没有找到想要的元素, 就返回没有找到(比如用 -1, 或者 None 表示)

线性查找的实现

#![allow(unused)]
fn main() {
#[must_use]
pub fn linear_search<T: PartialOrd>(slice: &[T], target: &T) -> Option<usize> {
    for (index, item) in slice.iter().enumerate() {
        if item == target {
            return Some(index);
        }
    }
    None
}
}

线性查找算法的特性

  • 该算法的时间复杂度是 O(N), 空间复杂度是 O(1).
  • 这个算法适合没有排序过的数组
  • 比较适合元素较少的数组
  • 不需要使用额外的内存
  • 因为是依次遍历元素, CPU 缓存命中率较高

二分查找 Binary Search

二分查找的步骤

实现二分查找法

二分查找法的特点

递归实现二分查找法

没有找到元素时, 返回期望的位置

二分查找法的边界值

二分查找相关的问题列表

容易

  1. 0035. 搜索插入位置 Search Insert Position
  2. 0069. x 的平方根 Sqrt(x)
  3. 0278. 第一个错误的版本
  4. 0349. 两个数组的交集 Intersection of Two Arrays
  5. 0374. 猜数字大小 Guess Number Higher or Lower
  6. 0704. 二分查找 Binary Search
  7. 0744. 寻找比目标字母大的最小字母 Find Smallest Letter Greater Than Target

TODO:

中等

  1. 0034. 在排序数组中查找元素的第一个和最后一个位置 Find First and Last Position of Element in Sorted Array
  2. 0074. 搜索二维矩阵 Search a 2D Matrix
  3. 0153. 寻找旋转排序数组中的最小值 Find Minimum in Rotated Sorted Array
  4. 0162. 寻找峰值 Find Peak Element
  5. 0167. 两数之和 II - 输入有序数组 Two Sum II - Input Array Is Sorted
  6. 0240. Search a 2D Matrix II Search a 2D Matrix II
  7. 0287. 寻找重复数 Find the Duplicate Number
  8. 0532. 数组中的数对 K-diff Pairs in an Array
  9. 0852. 山脉数组的峰顶索引 Peak Index in a Mountain Array
  10. 1498. 满足条件的子序列数目 Number of Subsequences That Satisfy the Given Sum Condition

TODO:

困难

  1. 0154. 寻找旋转排序数组中的最小值 II Find Minimum in Rotated Sorted Array II

TODO:

三元查找 Ternary Search

Jump Search

Interpolation Search

Exponential Search

标准库中二分查找法的实现

位运算 Bitwise Algorithms

比特位操作函数表:

ABA OR BA AND BA XOR BNOT A
111100
101010
011011
000001
  • BitOr, |
  • BitAnd, &
  • BitXor, ^
  • BitNot, ~
  • 左移, <<
  • 右移, >>

对自己异或操作结果为0

这个问题考察的是比特位异或操作中的一个重要特性: A XOR A == 0.

我们可以利用这个特性, 遍历数组中的每一项, 然后计算异或值, 最后的结果就是那个单值.

self-xor

Single Number

这个思路, 可以用于快速消除数组中出现偶数次的元素.

交换两个数值

或者, 不使用临时变量, 交换两个变量的值:

#![allow(unused)]
fn main() {
/// Swap two numbers without temporary variable.
///
/// ```rust
/// use bitwise::swap_number::swap_number;
///
/// let mut a = 3;
/// let mut b = 42;
/// swap_number(&mut a, &mut b);
/// assert_eq!(a, 42);
/// assert_eq!(b, 3);
/// ```
#[allow(clippy::manual_swap)]
pub fn swap_number(a: &mut i32, b: &mut i32) {
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}
}

递归 Recursion

排列与组合

矩阵 Matrix

质数

数独

任意精度算术运算

双指针 Two Pointers

双指针算法, 通常用于简化对数组(或者链表)的遍历, 可以只用一次遍历, 就能快速解决问题, 时间复杂度通常只有 O(n). 而无脑实现的暴力算法 (brute force), 通常需要内外两层遍历迭代, 其时间复杂度往往达到了 O(n^2).

可以说, 如果一个问题可以用双指针法解决的话, 其性能通常是很好的.

快慢型双指针

即访问数组(或者链表)时, 使用两个索引(或指针), 而不是通常的一个索引.

这两个指针, 分别称为快指针 (fast pointer) 和慢指针 (slow pointer).

快指针, 用于从0到n依次遍历整个数组, 即每次循环 fast += 1, 访问下一个元素

慢指针用于让数组中元素实现某个特定条件最高位索引, 比如件条可以是元素不重复. 当满足条件后, 要对数组做什么样的调整, 比如交换元素或者移除元素, 然后 slow += 1, 移动慢指针指向下一个元素. 当条件不满足时, 慢指针不动.

具体的过程看下图:

fast-slow

相关问题

靠拢型双指针问题

靠拢型双指针是指使用两个指针从左右两端访问数组, 往中间靠拢直到重叠.

close-up

靠拢型指针的一般步骤:

  • 初始化左右两个指针, 分别指向数组的左右两端 (left, right)
  • 开始遍历数组, 循环中止的条件就是两个指针重叠 ( left == right)
  • 根据题目要求, 选中左右两个指针中的一个, 往中间靠靠拢 (left += 1 或者 right -= 1, 另一个指针不动
  • 直到循环中止

Dutch National Flag, DNF

这是上面方法的一个变形, 可以查看问题 0075. 颜色分类 Sort Colors, 这个方法用于实现三路分区 (three-way partition).

The Dutch national flag

TODO(Shaohua): Add more description

相关问题

参考

并行双指针

这也是一类双指针问题. 用两个指针分别遍历两个数组(或者链表).

parallel

  • 初始化两个指针, 分别指向两个数组的头部元素
  • 如果条件成立, 就同时向右(高位)移动两个指针; 否则, 只移动其中一个, 比如 index1 += 1 或者 index2 += 1
  • 终止条件是, 直到有一个数组被遍历完

这个方法可以用来处理两个有序数组或者链表的合并; 或者计算两个集合的交集和并集.

相关问题

滑动窗口 Sliding Window

回溯法 Backtracking

分治法 Divide and Conquer

动态规划 Dynamic Programming

贪心算法 Greedy Algorithms

图算法

内存

缓存过期算法 Cache Management

倒计时 TTL

最近最少使用 LRU

最近最不频繁使用 LFU

限流算法 Rate limiter

令牌桶 Token bucket

漏桶算法 Leaking bucket

固定窗口计数 Fixed window counter

滑动窗口日志 Sliding window log

滑动窗口计数 Sliding window counter

leetcode 问题分类

这一节, 分别基于问题所属的标签, 和问题编号, 列出问题, 方便索引.

目前采用的刷题顺序:

  1. 数学
  2. 比特位操作
  3. 双指针
  4. 数组
  5. 链表
  6. 哈稀表
  7. 字符串
  8. 队列
  9. 回溯
  10. 贪心
  11. 动态规划

困难程度

难度知识点
入门数组, 字符串, 链表, 排序
简单栈, 队列, 哈希表, 双指针
中等二叉树, 堆, 单调栈, 滑动窗口, 二分, 位运算
困难DP, DFS, BFS, 回溯, 贪心, 并查集, 前缀树

数组相关的问题列表

容易

  1. 0066. 加一 Plus One
  2. 0485. 最大连续 1 的个数 Max Consecutive Ones
  3. 0724. 寻找数组的中心下标

TODO:

中级

TODO:

矩阵 Matrix

简单

  1. 0463. 岛屿的周长 Island Perimeter

中等

困难

前缀和数组 (Prefix Sum Array) 相关的问题列表

简单

  1. 0303. 区域和检索 - 数组不可变 Range Sum Query - Immutable
  2. 0724. 寻找数组的中心索引
  3. 1422. 分割字符串的最大得分 Maximum Score After Splitting a String
  4. 1480. 一维数组的动态和 Running Sum of 1d Array
  5. 1732. 找到最高海拔 Find the Highest Altitude
  6. 1854. 人口最多的年份 Maximum Population Year
  7. 1893. 检查是否区域内所有整数都被覆盖 Check if All the Integers in a Range Are Covered
  8. 1991. 找到数组的中间位置 Find the Middle Index in Array
  9. 2485. 找出中枢整数 Find the Pivot Integer
  10. 2574. 左右元素和的差值 Left and Right Sum Differences
  11. 2848. 与车相交的点 Points That Intersect With Cars
  12. 3028. 边界上的蚂蚁 Ant on the Boundary

中等

  1. 0238. 除自身以外数组的乘积 Product of Array Except Self

困难

双指针相关的问题列表

容易

  1. 0026. 删除有序数组中的重复项 Remove Duplicates from Sorted Array
  2. 0088. 合并两个有序数组 Merge Sorted Array
  3. 0125. 验证回文串 Valid Palindrome
  4. 0349. 两个数组的交集 Intersection of Two Arrays
  5. 0350. 两个数组的交集 II Intersection of Two Arrays II
  6. 0485. 最大连续1的个数 Max Consecutive Ones
  7. 0680. 验证回文串 II Valid Palindrome II
  8. 0925. 长按键入 Long Pressed Name
  9. 0977. 有序数组的平方 Squares of a Sorted Array
  10. 2108. 找出数组中的第一个回文字符串 Find First Palindromic String in the Array

TODO:

中等

  1. 0005. 最长回文子串 Longest Palindromic Substring
  2. 0011. 盛最多水的容器 Container With Most Water
  3. 0015. 三数之和 3Sum
  4. 0016. 最接近的三数之和 3Sum Closest
  5. 0031. 下一个排列 Next Permutation
  6. 0075. 颜色分类 Sort Colors
  7. 0080. 删除排序数组中的重复项 II Remove Duplicates from Sorted Array II
  8. 0167. 两数之和 II - 输入有序数组 Two Sum II - Input Array Is Sorted
  9. 0532.数组中的数对 K-diff Pairs in an Array

TODO:

滑动窗口相关的问题列表

简单

  1. 0485. 最大连续1的个数 Max Consecutive Ones

中等

  1. 0003. 无重复字符的最长子串 Longest Substring Without Repeating Characters
  2. 1004. 最大连续1的个数 III Max Consecutive Ones III

TODO:

困难

二分查找相关的问题列表

容易

  1. 0035. 搜索插入位置 Search Insert Position
  2. 0069. x 的平方根 Sqrt(x)
  3. 0278. 第一个错误的版本
  4. 0349. 两个数组的交集 Intersection of Two Arrays
  5. 0374. 猜数字大小 Guess Number Higher or Lower
  6. 0704. 二分查找 Binary Search
  7. 0744. 寻找比目标字母大的最小字母 Find Smallest Letter Greater Than Target

TODO:

中等

  1. 0034. 在排序数组中查找元素的第一个和最后一个位置 Find First and Last Position of Element in Sorted Array
  2. 0074. 搜索二维矩阵 Search a 2D Matrix
  3. 0153. 寻找旋转排序数组中的最小值 Find Minimum in Rotated Sorted Array
  4. 0162. 寻找峰值 Find Peak Element
  5. 0167. 两数之和 II - 输入有序数组 Two Sum II - Input Array Is Sorted
  6. 0240. Search a 2D Matrix II Search a 2D Matrix II
  7. 0287. 寻找重复数 Find the Duplicate Number
  8. 0532. 数组中的数对 K-diff Pairs in an Array
  9. 0852. 山脉数组的峰顶索引 Peak Index in a Mountain Array
  10. 1498. 满足条件的子序列数目 Number of Subsequences That Satisfy the Given Sum Condition

TODO:

困难

  1. 0154. 寻找旋转排序数组中的最小值 II Find Minimum in Rotated Sorted Array II

TODO:

排序

简单

中等

  1. 0056. 合并区间 Merge Intervals
  2. 0075. 颜色分类 Sort Colors
  3. 1498. 满足条件的子序列数目 Number of Subsequences That Satisfy the Given Sum Condition

TODO:

困难

链表相关的问题列表

容易

  1. 0234. 回文链表 Palindrome Linked List
  2. 0021.合并两个有序链表 Merge Two Sorted Lists
  3. 0083. 删除排序链表中的重复元素 Remove Duplicates from Sorted List

TODO:

中等

  1. 0002. 两数相加 Add Two Numbers
  2. 0082. 删除排序链表中的重复元素 II Remove Duplicates from Sorted List II
  3. 0707. 设计链表 Design Linked List

TODO:

困难

TODO:

参考

链表排序 Sorting

链表双指针 Two Pointers

队列 Queue

单调队列 Monotonic queue

栈相关的问题列表

容易

  1. 0020. 有效的括号 Valid Parentheses

TODO:

中等

  1. 0071. 简化路径 Simplify Path
  2. 0150. 逆波兰表达式求值 Evaluate Reverse Polish Notation
  3. 0155. 最小栈 Min Stack
  4. 0394. 字符串解码 Decode String

TODO:

困难

单调栈 Monotonic

简单

  1. 0496. 下一个更大元素 I Next Greater Element I

中等

  1. 0739. 每日温度 Daily Temperatures

困难

  1. 0042. 接雨水 Trapping Rain Water

优先级队列 Priority Queue

简单

中等

  1. 0347. 前 K 个高频元素 Top K Frequent Elements

困难

哈稀表相关的问题列表

容易

  1. 0001. 两数之和 Two Sum
  2. 0013. 罗马数字转整数 Roman to Integer
  3. 0217. 存在重复元素 Contains Duplicate
  4. 0219. 存在重复元素II Contains Duplicate II
  5. 0349. 两个数组的交集 Intersection of Two Arrays
  6. 0350. 两个数组的交集 II Intersection of Two Arrays II

TODO:

中等

  1. 0012. 整数转罗马数字 Integer to Roman
  2. 0167. 两数之和 II - 输入有序数组 Two Sum II - Input Array Is Sorted
  3. 0532.数组中的数对 K-diff Pairs in an Array

TODO:

字符串相关的问题列表

容易

TODO:

中级

  1. 0003. 无重复字符的最长子串 Longest Substring Without Repeating Characters
  2. 0468. 验证IP地址 Validate IP Address

TODO:

字符串匹配 String Matching

字典树 Trie

树相关的问题列表

容易

中等

二叉树的遍历 Traversal

二叉树的还原 Restore

二叉搜索树 Binary Search Tree

二叉索引树 Binary Indexed Tree

二叉索引树又称为树状数组.

线段树 Segment Tree

并查集 Union-Find Data Structure

图相关的问题列表

TODO(Shaohua): Update problems list

容易

中等

TODO:

最小生成树 Minimum Spanning Tree

递归相关的问题列表 Recursion

简单

中等

困难

位运算相关的问题列表

简单

  1. 0067. 二进制求和 Add Binary
  2. 0136. 只出现一次的数字 Single Number
  3. 0137. 只出现一次的数字II Single Number II
  4. 0191. 位1的个数 Number of 1 Bits
  5. 0231. 2的幂 Power of Two
  6. 0326. 3的幂 Power of Three
  7. 0338. 比特位计数 Counting Bits
  8. 0342. 4的幂 Power of Four

TODO:

中等

  1. 1780. 判断一个数字是否可以表示成三的幂的和 Check if Number is a Sum of Powers of Three

TODO:

困难

数学问题的问题列表

简单

  1. 1518. 换水问题 Water Bottles
  2. 2119. 反转两次的数字 A Number After a Double Reversal

TODO:

中等

  1. 0007. 整数反转 Reverse Integer
  2. 1780. 判断一个数字是否可以表示成三的幂的和 Check if Number is a Sum of Powers of Three

TODO:

回溯法相关的问题列表 Backtracking

简单

TODO:

中等

  1. 0039.组合总和 Combination Sum
  2. 0040. 组合总和 II Combination Sum II
  3. 0046. 全排列 Permutations
  4. 0047. 全排列 II Permutations II
  5. 0078. 子集 Subsets
  6. 0090. 子集 II Subsets II

TODO:

困难

  1. 0679. 24 点游戏 24 Game

分治法相关的问题列表 Divide and Conquer

简单

中等

困难

深度优先搜索 Depth First Search, DFS

简单

中等

困难

广度优先搜索 Bridth First Search, BFS

简单

中等

  1. 0322. 零钱兑换 Coin Change

困难

动态规划相关的问题列表 Dynamic Programming

容易

TODO:

中等

  1. 0062. 不同路径 Unique Paths
  2. 0322. 零钱兑换 Coin Change

TODO:

困难

TODO:

贪心算法相关的问题列表

容易

中等

leetcode 问题分类

这一节, 分别基于问题所属的标签, 和问题编号, 列出问题, 方便索引.

目前采用的刷题顺序:

  1. 数学
  2. 比特位操作
  3. 双指针
  4. 数组
  5. 链表
  6. 哈稀表
  7. 字符串
  8. 队列
  9. 回溯
  10. 贪心
  11. 动态规划

困难程度

难度知识点
入门数组, 字符串, 链表, 排序
简单栈, 队列, 哈希表, 双指针
中等二叉树, 堆, 单调栈, 滑动窗口, 二分, 位运算
困难DP, DFS, BFS, 回溯, 贪心, 并查集, 前缀树

问题 0001-0100

0001. 两数之和 Two Sum

问题描述

方法1, Brute Force

这个方法比较直接, 就是遍历数组, 并遍历后面的每个元素, 求两者之和是否与 target 相等.

brute-force

因为有两层遍历, 这个方法的时间复杂度是 O(n^2).

#![allow(unused)]
fn main() {
// Brute Force
fn two_sum1(nums: Vec<i32>, target: i32) -> Vec<i32> {
    let len = nums.len();
    for i in 0..(len - 1) {
        for j in (i + 1)..len {
            if i != j && nums[i] + nums[j] == target {
                return vec![i as i32, j as i32];
            }
        }
    }
    Vec::new()
}
}

方法2, 哈稀表

同样是需要遍历整个数组, 我们可以使用哈稀表缓存一下访问过的元素, 以加快查找元素的时间. 这个哈稀表用于记录元素值到它在数组中的索引值之间的关系.

但对于从哈稀表中查找, 我们可以进行一下优化, 即, 查找与当前元素之和为 target 的值, 如果找到, 就可以返回了.

hash-table

这个方法的时间复杂度是 O(n).

#![allow(unused)]
fn main() {
// 使用 Hash Table
fn two_sum2(nums: Vec<i32>, target: i32) -> Vec<i32> {
    use std::collections::HashMap;

    let mut visited: HashMap<i32, usize> = HashMap::with_capacity(nums.len());
    // 遍历整个数组.
    for (i, &item) in nums.iter().enumerate() {
        // 查找与当前元素之和为 target 的元素, 是否在哈稀表中.
        if let Some(&j) = visited.get(&(target - item)) {
            return vec![j as i32, i as i32];
        } else {
            visited.insert(item, i);
        }
    }

    Vec::new()
}
}

0002. 两数相加 Add Two Numbers

问题描述

0003. 无重复字符的最长子串 Longest Substring Without Repeating Characters

问题描述

这类问题, 可以优先考虑使用滑动窗口, 因为要找最长无重复的子串, 可以在窗口内维护那个子串, 然后将窗口右侧向右移, 如果条件不满足, 就把窗口左侧边界向右移, 直到满足条件为止.

代码实现:

#![allow(unused)]
fn main() {
use std::collections::HashMap;

// 滑动窗口法
// 使用HashMap 来缓存窗口内的所有字符, 加快查找
pub fn length_of_longest_substring2(s: String) -> i32 {
    // 当窗口右侧经过一个字符时, 要检查它是不是在窗口内.
    // 如果不在窗口内, 则继续当窗口右侧右移;
    // 如果在窗口内了, 说明需要将窗口左侧向右移, 直到遇到相同的字符.
    // 然后计算最长的那个窗口.

    // (字符, 字符在字符串中的位置)
    let mut map = HashMap::<u8, usize>::new();
    let bytes = s.as_bytes();

    let mut substring_max_len = 0;
    let mut left = 0;
    let mut right = 0;

    // 遍历所有字符.
    while right < bytes.len() {
        if let Some(&index) = map.get(&bytes[right]) {
            while left <= index {
                map.remove(&bytes[left]);
                left += 1;
            }
        }

        // 将窗口右侧字符加进去.
        map.insert(bytes[right], right);
        right += 1;

        // 并更新最大字串.
        substring_max_len = substring_max_len.max(map.len());
    }

    // 返回结果
    substring_max_len as i32
}
}

寻找两个正序数组的中位数 Median of Two Sorted Arrays

问题描述

0005. 最长回文子串 Longest Palindromic Substring

问题描述

这个问题, 要充分考虑回文字符串的特性:

  • 如果回文字符串有奇数个字符, 那就以中间那个字符为分界, 左右两侧完全相同
  • 如果回文字符串有偶数个字符, 那中间两个字符相同, 并且它们左右两侧也完全相同

基于这个特性, 可以利用双指针法来找出回文子串, 具体步骤是:

  • 先遍历字符串中的每个字符
  • 并以它为中心字符 (middle), 利用远离型双指针, 分别向左右两侧扩张, 并且 s[left] == s[right]
  • 接下来考虑偶数个字符的情况, 以 (middle, middle + 1) 为中心, 利用远离型双指针, 分别向左右两侧扩张
  • 通过这个循环, 就可以找出字符串 s 中的所有回文子串
  • 然后保存下来最长的那个子串, 既是最终的结果

代码实现如下:

Rust

#![allow(unused)]
fn main() {
// 远离型双指针
pub fn longest_palindrome1(s: String) -> String {
    fn two_sides_equal(s: &str, left: usize, right: usize) -> bool {
        // 先检查两个索引值的有效性, 再比较它们指向的字符是否相等.
        right < s.len() && s.as_bytes().get(left) == s.as_bytes().get(right)
    }

    let mut longest_palindrome_len = 0;
    let mut longest_palindrome_start = 0;

    // 遍历所有字符
    for middle in 0..s.len() {
        // 以 (middle) 为对称点, substring 有奇数个字符
        let mut left = middle;
        let mut right = middle;
        while two_sides_equal(&s, left, right) {
            let length = right - left + 1;
            if longest_palindrome_len < length {
                longest_palindrome_len = length;
                longest_palindrome_start = left;
            }
            if left == 0 {
                break;
            }
            left -= 1;
            right += 1;
        }

        // 以 (middle, middle+1) 作为对称点, substring 有偶数个字符
        left = middle;
        right = middle + 1;
        while two_sides_equal(&s, left, right) {
            let length = right - left + 1;
            if longest_palindrome_len < length {
                longest_palindrome_len = length;
                longest_palindrome_start = left;
            }
            if left == 0 {
                break;
            }
            left -= 1;
            right += 1;
        }
    }

    // 返回结果
    s[longest_palindrome_start..longest_palindrome_start + longest_palindrome_len].to_owned()
}
}

Python

def longestPalindrome(s: str) -> str:
    # 跟据回文字符串的特性

    def twoSidesEqual(s: str, left: int, right: int) -> bool:
        # 先检查两个索引值的有效性, 再比较它们指向的字符是否相等.
        if left >= 0 and right < len(s) and s[left] == s[right]:
            return True
        else:
            return False

    longest_palindrome_len = 0
    longest_palindrome_start = 0

    # 遍历所有字符
    for middle in range(len(s)):
        # 以 (middle) 为对称点, substring 有奇数个字符
        left = middle
        right = middle
        while twoSidesEqual(s, left, right):
            length = right - left + 1
            if longest_palindrome_len < length:
                longest_palindrome_len = length
                longest_palindrome_start = left
            left -= 1
            right += 1

        # 以 (middle, middle+1) 作为对称点, substring 有偶数个字符
        left = middle
        right = middle + 1
        while twoSidesEqual(s, left, right):
            length = right - left + 1
            if longest_palindrome_len < length:
                longest_palindrome_len = length
                longest_palindrome_start = left
            left -= 1
            right += 1

    # 返回结果
    return s[longest_palindrome_start:longest_palindrome_start+longest_palindrome_len]

def main():
    s1 = "babad"
    out1 = longestPalindrome(s1)
    assert(out1 == "bab")

    s2 = "cbbd"
    out2 = longestPalindrome(s2)
    assert(out2 == "bb")

if __name__ == "__main__":
    main()

0007. 整数反转 Reverse Integer

问题描述

这是一个数学问题. 它考察的知识有这几个方面:

  1. 如何从十进制的整数中提取出各个位的值
  2. 如何从各个位的值重新组装一个整数
  3. 如何处理整数边界溢出的问题

接下来我们分别来说说.

从十进制的整数中提取出各个位的值

要使用 除10 的操作, 进行十进制的右移.

#![allow(unused)]
fn main() {
let mut x = 1234;
while x != 0 {
  println!("unit value: {}", x % 10);
  x /= 10;
} 
}

看图:

get-unit-digit

重新组装整数

如何从各个位的值重新组装一个整数? 使用相反的操作, 乘10, 进行十进制的左移操作.

#![allow(unused)]
fn main() {
let mut number = 0;
let bits = &[4, 3, 2, 1];
for bit in bits {
  number = number * 10 + bit;
}
println!("number: {number}");
}

看图:

assemble-integer

整数溢出

如何处理整数边界溢出的问题? 在组装新的整数时, 可以让当前的值 *10 后与 i32::MAX .. i32::MIN 进行比较, 看在不在这个范围内.

number > i32::MAX / 10 || number < i32::MIN / 10

integer-in-range

要注意的一点是, 不能使用 number * 10 > i32::MAX 这样的写法, 因为 number * 10 本身就可能溢出了!

基于以上3点, 编写出最终的代码:

#![allow(unused)]
fn main() {
#[allow(clippy::manual_range_contains)]
pub fn reverse(x: i32) -> i32 {
    let mut x = x;
    let mut rev = 0;

    // x == 0 时, 表示它的所有整数进位值都被提取完了.
    while x != 0 {
        // 检查 rev 在添加新的个位值后是否会溢出
        if rev > i32::MAX / 10 || rev < i32::MIN / 10 {
            return 0;
        }
        // 从 x 中提取出个位的值, 然后作为新的个位数值, 添加到 rev 上.
        rev = rev * 10 + x % 10;
        x /= 10;
    }
    rev
}
}

0011. 盛最多水的容器 Container With Most Water

问题描述

这是一个典型的靠拢型双指针问题.

#![allow(unused)]
fn main() {
// 靠拢型双指针
pub fn max_area1(height: Vec<i32>) -> i32 {
    let len = height.len();
    assert!(len > 1);

    let mut max_area = 0;
    // 两个指针分别从数组的左右两头开始, 往中间靠拢
    let mut left = 0;
    let mut right = len - 1;

    // 循环中止条件是左右两个指针重叠
    while left < right {
        let area: i32;
        if height[left] < height[right] {
            area = (right - left) as i32 * height[left];
            // 一次循环只移动一个指针
            left += 1;
        } else {
            area = (right - left) as i32 * height[right];
            right -= 1;
        }
        // 目标就是找到最大面积
        max_area = area.max(max_area);
    }

    max_area
}
}

针对靠拢型双指针做一点优化

上面提到的代码实现是经典的写法, 但这里, 还可以针对里面的实现细节做一些优化. 具体来说就是, 判断当前指针的下个元素的值如果大不于当前值, 那就可以跳过下个元素, 因为它的面积值一定比当前根据两个指针计算的面积值要小.

#![allow(unused)]
fn main() {
// 优化靠拢型双指针
pub fn max_area2(height: Vec<i32>) -> i32 {
    let len = height.len();
    assert!(len > 1);

    let mut max_area = 0;
    // 两个指针分别从数组的左右两头开始, 往中间靠拢
    let mut left = 0;
    let mut right = len - 1;

    // 循环中止条件是左右两个指针重叠
    while left < right {
        if height[left] < height[right] {
            let curr = height[left];
            let area = (right - left) as i32 * curr;
            // 目标就是找到最大面积
            max_area = area.max(max_area);

            // 内层循环用于跳过无效的高度
            while (left < right) && (height[left] <= curr) {
                left += 1;
            }
        } else {
            let curr = height[right];
            let area = (right - left) as i32 * curr;
            // 目标就是找到最大面积
            max_area = area.max(max_area);

            while (left < right) && (height[right] <= curr) {
                right -= 1;
            }
        }
    }

    max_area
}
}

0012. 整数转罗马数字 Integer to Roman

问题描述

这也是字典映射问题, 目前实现了三种解法

模式匹配

这个类似于哈稀表, 但是性能更高.

#![allow(unused)]
fn main() {
#[must_use]
#[inline]
pub const fn get_more_roman_symbols(num: i32) -> &'static str {
    match num {
        3000 => "MMM",
        2000 => "MM",
        1000 => "M",
        900 => "CM",
        800 => "DCCC",
        700 => "DCC",
        600 => "DC",
        500 => "D",
        400 => "CD",
        300 => "CCC",
        200 => "CC",
        100 => "C",
        90 => "XC",
        80 => "LXXX",
        70 => "LXX",
        60 => "LX",
        50 => "L",
        40 => "XL",
        30 => "XXX",
        20 => "XX",
        10 => "X",
        9 => "IX",
        8 => "VIII",
        7 => "VII",
        6 => "VI",
        5 => "V",
        4 => "IV",
        3 => "III",
        2 => "II",
        1 => "I",
        _ => "-",
    }
}

// 模式匹配
pub fn int_to_roman1(num: i32) -> String {
    assert!((0..=3999).contains(&num));

    let mut num = num;
    let mut quot;
    // 最大支持1000级的
    let mut factor = 1000;
    let mut out = String::new();

    while num > 0 || factor > 0 {
        // 计算商
        quot = num / factor;
        // 构造这个位的数值.
        let val = quot * factor;

        if val > 0 {
            // 根据数值, 拿到对应的字符串
            out += get_more_roman_symbols(val);
        }

        // 计算余数, 准备下一次循环使用
        num %= factor;
        factor /= 10;
    }

    out
}
}

分段映射

单独对每个进位做映射, 很简单的实现, 易懂.

split-digits

#![allow(unused)]
fn main() {
// 单独分开各个进位的值
pub fn int_to_roman3(num: i32) -> String {
    const ONES: &[&str] = &["", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"];
    const TENS: &[&str] = &["", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"];
    const HUNDREDS: &[&str] = &["", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"];
    const THOUSANDS: &[&str] = &["", "M", "MM", "MMM"];

    let num = num as usize;
    let parts = [
        THOUSANDS[num / 1000],
        HUNDREDS[(num % 1000) / 100],
        TENS[(num % 100) / 10],
        ONES[num % 10],
    ];
    parts.join("").to_owned()
}
}

手动匹配

这种方法支持更复杂的整数, 扩展性更好, 但是比较复杂, 性能也一般

#![allow(unused)]
fn main() {
#[must_use]
#[inline]
pub const fn get_roman_symbol(num: i32) -> char {
    match num {
        1000 => 'M',
        500 => 'D',
        100 => 'C',
        50 => 'L',
        10 => 'X',
        5 => 'V',
        1 => 'I',
        _ => '-',
    }
}

#[must_use]
pub fn get_roman_symbols_manual(quot: i32, factor: i32) -> String {
    match quot {
        0 => String::new(),
        val @ 1..=3 => {
            let one = get_roman_symbol(factor);
            let mut s = String::new();
            // [one]
            // [one, one]
            // [one, one, one]
            for _i in 0..val {
                s.push(one);
            }
            s
        }
        4 => {
            let five = get_roman_symbol(5 * factor);
            let one = get_roman_symbol(factor);
            let mut s = String::new();
            // [one, five]
            s.push(one);
            s.push(five);
            s
        }
        5 => get_roman_symbol(5 * factor).to_string(),
        val @ 6..=8 => {
            let five = get_roman_symbol(5 * factor);
            let one = get_roman_symbol(factor);
            let mut s = String::new();
            // [five, one]
            // [five, one, one]
            // [five, one, one, one]
            s.push(five);
            for _i in 0..(val - 5) {
                s.push(one);
            }
            s
        }
        9 => {
            let ten = get_roman_symbol(10 * factor);
            let one = get_roman_symbol(factor);
            let mut s = String::new();
            // [one, ten]
            s.push(one);
            s.push(ten);
            s
        }
        _ => unimplemented!(),
    }
}

// 部分模式匹配
// 剩下的手动计算, 性能较差
pub fn int_to_roman2(num: i32) -> String {
    assert!((0..=3999).contains(&num));

    let mut num = num;
    let mut quot;
    // 最大支持1000级的
    let mut factor = 1000;
    let mut out = String::new();

    while num > 0 || factor > 0 {
        // 计算商
        quot = num / factor;

        if quot > 0 {
            // 构造这个位的数值对应的罗马数字.
            out += &get_roman_symbols_manual(quot, factor);
        }

        // 计算余数, 准备下一次循环使用
        num %= factor;
        factor /= 10;
    }

    out
}
}

相关问题

0013. 罗马数字转整数 Roman to Integer

问题描述

这是一个符号到整数的映射问题.

处理映射问题, 通常使用哈稀表, 如果元素比较少的话, 也可以使用 Rust 特有的模式匹配, 可以提高运行速度, 减少堆内存占用.

哈稀表

#![allow(unused)]
fn main() {
// 哈稀表
pub fn roman_to_int1(s: String) -> i32 {
    assert!(!s.is_empty());

    // 使用哈稀表处理罗马数字到阿拉伯数字的映射.
    let map = HashMap::<u8, i32>::from([
        (b'I', 1),
        (b'V', 5),
        (b'X', 10),
        (b'L', 50),
        (b'C', 100),
        (b'D', 500),
        (b'M', 1000),
    ]);

    let bytes = s.as_bytes();
    let mut sum = 0;

    for i in 0..bytes.len() {
        // 获取当前元素的数值
        let val: i32 = *map.get(&bytes[i]).unwrap();

        // 获取下个元素的数值
        let next_val: i32 = if i + 1 < bytes.len() {
            *map.get(&bytes[i + 1]).unwrap()
        } else {
            0
        };

        // 根据val, next_val 的关系, 确定采用加法还是减法
        if val >= next_val {
            // 如果是加法
            sum += val;
        } else {
            // 如果是减法
            sum -= val;
        }
    }

    sum
}
}

模式匹配

roman-to-int

#![allow(unused)]
fn main() {
    // 辅助函数, 处理罗马数字到阿拉伯数字的映射.
    #[must_use]
    #[inline]
    const fn get_roman_number(symbol: char) -> i32 {
        match symbol {
            'I' => 1,
            'V' => 5,
            'X' => 10,
            'L' => 50,
            'C' => 100,
            'D' => 500,
            'M' => 1000,
            _ => 0,
        }
    }
}

完整的代码如下:

#![allow(unused)]
fn main() {
// 模式匹配
// 比哈稀表快, 没有分配堆内存
pub fn roman_to_int2(s: String) -> i32 {
    // 辅助函数, 处理罗马数字到阿拉伯数字的映射.
    #[must_use]
    #[inline]
    const fn get_roman_number(symbol: char) -> i32 {
        match symbol {
            'I' => 1,
            'V' => 5,
            'X' => 10,
            'L' => 50,
            'C' => 100,
            'D' => 500,
            'M' => 1000,
            _ => 0,
        }
    }

    assert!(!s.is_empty());
    let mut sum = 0;
    let mut curr_val = 0;
    let mut prev_val = 0;

    for symbol in s.chars() {
        // 获取当前元素的数值
        curr_val = get_roman_number(symbol);

        // 根据val, next_val 的关系, 确定采用加法还是减法
        if curr_val > prev_val {
            // 如果是减法
            sum -= prev_val;
        } else {
            // 如果是加法
            sum += prev_val;
        }
        prev_val = curr_val;
    }

    // 别忘了加上最后一个数值.
    sum += curr_val;

    sum
}
}

0015. 三数之和 3Sum

问题描述

暴力法

直接用多层遍历的方法, 暴力解决问题. 但这个方法的时间复杂度为 O(n^3), 性能也是最差的.

3-loops

#![allow(unused)]
fn main() {
// 暴力法
// 这个方法时间复杂度为 O(n^3), 空间复杂度为 O(n)
// 计算结果超时
pub fn three_sum1(nums: Vec<i32>) -> Vec<Vec<i32>> {
    let mut nums = nums;
    nums.sort();
    let len = nums.len();
    if len < 3 {
        return vec![];
    }

    let mut set = HashSet::new();

    for i in 0..(len - 2) {
        for j in (i + 1)..(len - 1) {
            for k in (j + 1)..len {
                if nums[i] + nums[j] + nums[k] == 0 {
                    set.insert(vec![nums[i], nums[j], nums[k]]);
                }
            }
        }
    }
    set.into_iter().collect()
}
}

靠拢型双指针

相关的方法介绍可以看这里.

  • 先对数组进行排序
  • 外层循环遍历整个数组, 然后在内层使靠拢型用双指针, 来快速找到元素组合
  • 可以跳过重复的元素

该算法的时间复杂度是 O(n^2), 空间复杂度是 O(n).

two-pointers-in-a-loop

Rust

#![allow(unused)]
fn main() {
// 靠拢型双指针
// 这个方法时间复杂度为 O(n^2), 空间复杂度为 O(1)
pub fn three_sum2(nums: Vec<i32>) -> Vec<Vec<i32>> {
    let mut result = Vec::new();
    let len = nums.len();
    if len < 3 {
        return result;
    }

    // 先排序
    let mut nums = nums;
    nums.sort_unstable();

    // 遍历数组
    for i in 0..(len - 2) {
        let first = nums[i];

        // 忽略掉第一个元素大于0的值
        if first > 0 {
            break;
        }
        // 跳过重复的元素
        if i > 0 && nums[i] == nums[i - 1] {
            continue;
        }

        // 初始化双指针, 分别指向子数组的左右两端.
        let mut left = i + 1;
        let mut right = len - 1;

        // 循环中止的条件是两指针重叠.
        while left < right {
            let sum = first + nums[left] + nums[right];
            match sum.cmp(&0) {
                // 移动其中的一个指针
                Ordering::Less => left += 1,
                Ordering::Greater => right -= 1,
                Ordering::Equal => {
                    // 其和等于0, 找到目标元素.
                    result.push(vec![first, nums[left], nums[right]]);

                    let last_left_val = nums[left];
                    // 从左右两端分别跳过重复的元素.
                    while left < right && nums[left] == last_left_val {
                        left += 1;
                    }
                    let last_right_val = nums[right];
                    while left < right && nums[right] == last_right_val {
                        right -= 1;
                    }
                }
            }
        }
    }

    result
}
}

C++

#include <cassert>
#include <cstdio>

#include <algorithm>
#include <vector>
#include <set>

class Solution {
 public:
  // 双指针法
  std::vector<std::vector<int>> threeSum(std::vector<int>& nums) {
    // 先对整数数组排序
    std::sort(nums.begin(), nums.end());
    std::vector<std::vector<int>> result;

    // 最外层, 遍历所有整数
    for (size_t i = 0; i < nums.size() - 2; ++i) {
      // 如果最小的值都比0大了, 那就不用再检查后面的值
      if (nums[i] > 0) {
        break;
      }
      // 可以跳过重复的元素
      if (i > 0 && nums[i] == nums[i-1]) {
        continue;
      }

      // 初始化双指针
      int left = i + 1;
      int right = nums.size() - 1;
      int first = nums[i];

      while (left < right) {
        int sum = first + nums[left] + nums[right];
        if (sum < 0) {
          // 大小了
          left += 1;
        } else if (sum > 0) {
          // 太大了
          right -= 1;
        } else {
          // 等于0
          result.push_back({first, nums[left], nums[right]});
          // 路过左侧重复的元素
          while (left < right && nums[left] == nums[left + 1]) {
            left += 1;
          }
          // 路过右侧重复的元素
          while (right > left && nums[right] == nums[right - 1]) {
            right -= 1;
          }
          // 移动双指针, 向中间靠拢
          left += 1;
          right -= 1;
        }
      }
    }

    return result;
  }
};

哈稀表

这个方法的性能并不好, 但也算是解决问题的一个思路. 具体做法就是:

  1. 使用两个字典分别存储大于0, 以及小于0的元素; 同时统计0出现的次数
  2. 首先如果0存在, 尝试用它作为正负数的分隔, 其组合情况如下:
    • (负数, 0, 正数)
    • (0, 0, 0)
  3. 现在考虑非0组合, 可能的组合情况如下:
    • (负数, 负数, 正数)
    • (负数, 正数, 正数)
  4. 将所有的组合都添加到一个集合中, 这样就可以用它来去掉重复的组合

这种思路, 也同样可以用于解决 target != 0 的情况.

#![allow(unused)]
fn main() {
// 哈稀表
pub fn three_sum4(nums: Vec<i32>) -> Vec<Vec<i32>> {
    // 0出现的次数.
    let mut zeros = 0;
    // 使用两个字典分别存储大于0, 以及小于0的元素
    let mut negatives = HashMap::new();
    let mut positives = HashMap::new();

    for &num in &nums {
        match num.cmp(&0) {
            Ordering::Less => {
                negatives
                    .entry(num)
                    .and_modify(|count| *count += 1)
                    .or_insert(1);
            }
            Ordering::Equal => zeros += 1,
            Ordering::Greater => {
                positives
                    .entry(num)
                    .and_modify(|count| *count += 1)
                    .or_insert(1);
            }
        }
    }

    // 使用集合来过滤到重复的结果.
    let mut set = HashSet::new();

    // 首先如果0存在, 就用它作为正负数的分隔.
    if zeros > 0 {
        for &num in negatives.keys() {
            if positives.contains_key(&(-num)) {
                set.insert(vec![num, 0, -num]);
            }
        }

        if zeros > 2 {
            set.insert(vec![0, 0, 0]);
        }
    }

    // 现在考虑非0组合, 可能的情竞是:
    // - (负数, 负数, 正数)
    // - (负数, 正数, 正数)

    for &negative in negatives.keys() {
        for &positive in positives.keys() {
            let expected_num = -(positive + negative);
            match expected_num.cmp(&0) {
                Ordering::Less => {
                    if let Some(&count) = negatives.get(&expected_num) {
                        if (count > 1) || negative != expected_num {
                            if negative < expected_num {
                                set.insert(vec![negative, expected_num, positive]);
                            } else {
                                set.insert(vec![expected_num, negative, positive]);
                            }
                        }
                    }
                }
                Ordering::Greater => {
                    if let Some(&count) = positives.get(&expected_num) {
                        if (count > 1) || positive != expected_num {
                            if positive < expected_num {
                                set.insert(vec![negative, positive, expected_num]);
                            } else {
                                set.insert(vec![negative, expected_num, positive]);
                            }
                        }
                    }
                }
                Ordering::Equal => (),
            }
        }
    }

}

0016. 最接近的三数之和 3Sum Closest

问题描述

双指针法

可以使用双指针法遍历整个数组. 具体的步骤如下:

  • 先对整数数组进行排序
  • 然后计算特殊情况
    • 整数数组长度为3时, 直接返回前三个元素的和
    • 前三个元素的和等于 target 时, 也可以直接返回
  • 遍历数组中所有元素(一直到倒数第三个元素), 作为三个数中的第一个数
    • 对于第二个和第三个数, 我们使用双指针法来遍历
    • 先计算三个元素的和 sum, 以及 sum 与 target 的差值绝对值 diff
    • 如果 sum == target, 直接返回
    • 如果 sum < target, 将双指针的左侧元素向右移一位, left += 1
    • 如果 sum > target, 将双指针的右侧元素向左移一位, right -= 1
    • 如果 diff 比目前最小的差值还小, 那就更新它

代码实现如下:

Rust

#![allow(unused)]
fn main() {
use std::cmp::Ordering;

// 靠拢型双指针
pub fn three_sum_closest2(nums: Vec<i32>, target: i32) -> i32 {
    let len = nums.len();
    assert!(nums.len() >= 3);

    // 对数组排序.
    let mut nums = nums;
    nums.sort();

    // 先处理特别情况.
    let mut closest: i32 = nums[0] + nums[1] + nums[2];
    if len == 3 || closest == target {
        return closest;
    }
    let mut closest_diff: i32 = i32::MAX;

    // 遍历数组.
    for i in 0..(len - 2) {
        // 初始化双指针.
        let mut left = i + 1;
        let mut right = len - 1;
        while left < right {
            // 不需要检查整数溢出.
            let sum = nums[i] + nums[left] + nums[right];
            match sum.cmp(&target) {
                // 如果找到了与 `target` 相同的结果, 就不需要再循环了, 直接返回.
                Ordering::Equal => return sum,
                // 移动指针
                Ordering::Less => left += 1,
                Ordering::Greater => right -= 1,
            }

            let diff = (sum - target).abs();
            if diff < closest_diff {
                // 更新新的最接近值.
                closest = sum;
                closest_diff = diff;
            }
        }
    }

    closest
}
}

C++

#include <cassert>
#include <climits>

#include <algorithm>
#include <vector>

class Solution {
 public:
  static
  int threeSumClosest(std::vector<int>& nums, int target) {
    // 先排序
    std::sort(nums.begin(), nums.end());
    assert(nums.size() >= 3);

    int closest = nums[0] + nums[1] + nums[2];
    int min_diff = INT_MAX;
    if (nums.size() == 3 || closest == target) {
      return closest;
    }

    // 遍历数组
    for (size_t i = 0; i < nums.size() - 2; ++i) {
      // 初始化双指针
      int left = i + 1;
      int right = nums.size() - 1;
      int first = nums[i];

      while (left < right) {
        int sum = first + nums[left] + nums[right];
        // 如果与 target 相等, 就直接返回.
        if (sum == target) {
          return sum;
        } else if (sum < target) {
          left += 1;
        } else if (sum > target) {
          right -= 1;
        }

        const int diff = std::abs(sum - target);
        if (diff < min_diff) {
          // 更新最小差值
          min_diff = diff;
          closest = sum;
        }
      }
    }

    return closest;
  }
};

0020. 有效的括号 Valid Parentheses

问题描述

这个问题使用栈来解决, 思路有两种:

  • 遇到左侧的括号, 就把它入栈; 遇到右侧括号, 就将栈顶元素出栈, 并比对它们是否成对
  • 遇到左侧的括号, 就把与它对应的右侧括号入栈; 遇到右侧括号, 就将栈顶元素出栈, 并判断它们是否相同

代码实现

Rust

#![allow(unused)]
fn main() {
pub fn is_valid2(s: String) -> bool {
    let mut stack = Vec::<char>::new();
    for bracket in s.chars() {
        match bracket {
            // 先匹配左侧的括号, 并把与之成对的右侧括号入栈.
            '(' => stack.push(')'),
            '[' => stack.push(']'),
            '{' => stack.push('}'),
            // 如果遇到右侧括号, 就把它与栈顶元素比较, 看是否相同.
            // 使用 match-guard
            ')' | ']' | '}' if Some(bracket) != stack.pop() => return false,
            _ => (),
        }
    }

    stack.is_empty()
}
}

C++

bool isValid2(std::string s) {
  std::stack<char> stack;

  for (char c : s) {
    // 先匹配左侧的括号, 并把与之成对的右侧括号入栈.
    if (c == '(') {
      stack.push(')');
    } else if (c == '[') {
      stack.push(']');
    } else if (c == '{') {
      stack.push('}');
    } else if (stack.empty()) {
      return false;
    } else {
      // 如果遇到右侧括号, 就把它与栈顶元素比较, 看是否相同.
      char old_c = stack.top();
      stack.pop();
      if (old_c != c) {
        return false;
      }
    }
  }

  return stack.empty();
}

0021.合并两个有序链表 Merge Two Sorted Lists

问题描述

0026. 删除有序数组中的重复项 Remove Duplicates from Sorted Array

问题描述

要注意的一点是, 这个数组已经是有序的了.

方法1, 快慢型双指针法

快慢型双指针法的详细说明, 可以参考这里.

two-pointers

#![allow(unused)]
fn main() {
// 快慢型双指针 Two pointers
#[allow(clippy::ptr_arg)]
pub fn remove_duplicates2(nums: &mut Vec<i32>) -> i32 {
    assert!(!nums.is_empty());
    // 第一个指针, 用于记录当前不重复的位置
    let mut slow_idx = 1;
    // 第二个指针, 用于遍历数组
    for fast_idx in 1..nums.len() {
        if nums[fast_idx - 1] != nums[fast_idx] {
            nums[slow_idx] = nums[fast_idx];
            slow_idx += 1;
        }
    }
    slow_idx as i32
}
}

方法2, 使用 Vec 自带的去重方法

Vec::dedup() 就是用来去重的, 如果它已经是排序的了, 可以去掉所有重复元素.

#![allow(unused)]
fn main() {
// Vec 的去重函数, 支持已排序的
pub fn remove_duplicates3(nums: &mut Vec<i32>) -> i32 {
    nums.dedup();
    nums.len() as i32
}
}

相关问题

0031. 下一个排列 Next Permutation

问题描述

TODO(Shaohua):

在排序数组中查找元素的第一个和最后一个位置 Find First and Last Position of Element in Sorted Array

问题描述

这个问题适合用二分查找法.

基本的二分查找法

分两步来解这个问题:

  1. 使用二分查找法确定 target 存在于数组中, 如果不存在就直接返回; 如果存在, 就得到了它的索引值 middle
  2. 然后使用线性查找法, 分别从 middle 开始向数组的左右两端查找与 target 元素相等的

最后的实现代码如下所示:

#![allow(unused)]
fn main() {
use std::cmp::Ordering;

// Binary search
pub fn search_range1(nums: Vec<i32>, target: i32) -> Vec<i32> {
    let mut result = vec![-1, -1];
    let len = nums.len();

    // 处理极端情况
    if len == 0 || nums[0] > target || nums[len - 1] < target {
        return result;
    }

    let mut low = 0;
    let mut high = len - 1;
    let mut middle = 0;
    while low <= high {
        middle = low + (high - low) / 2;
        match nums[middle].cmp(&target) {
            Ordering::Less => low = middle + 1,
            Ordering::Equal => break,
            Ordering::Greater => {
                if middle > 1 {
                    high = middle - 1;
                } else {
                    // 没有找到
                    return result;
                }
            }
        }
    }

    // 退化成线性查找
    let mut i = middle as i32;
    while i >= 0 && nums[i as usize] == target {
        result[0] = i;
        i -= 1;
    }
    let mut i = middle;
    while i < len && nums[i] == target {
        result[1] = i as i32;
        i += 1;
    }
    result
}
}

这个是对上述方法的简化, 使用标准库中自带的二分查找算法:

#![allow(unused)]
fn main() {
// 使用 slice::binary_search()
pub fn search_range2(nums: Vec<i32>, target: i32) -> Vec<i32> {
    let mut result = vec![-1, -1];
    let len = nums.len();

    // 处理极端情况
    if len == 0 || nums[0] > target || nums[len - 1] < target {
        return result;
    }

    let middle = match nums.binary_search(&target) {
        Ok(index) => index,
        Err(_) => return result,
    };

    // 退化成线性查找
    let mut i = middle as i32;
    while i >= 0 && nums[i as usize] == target {
        result[0] = i;
        i -= 1;
    }
    let mut i = middle;
    while i < len && nums[i] == target {
        result[1] = i as i32;
        i += 1;
    }
    result
}
}

两次二分查找

上面的算法, 对于查找与 target 相等的元素时, 使用了两次线性查找, 用于确定左右边界. 这一步时间复杂度是 O(k), 其中的 k 是与 target 相等的元素的个数.

我们可以使用两次二分查找法, 取代线性查找法, 直接确定左右边界.

#![allow(unused)]
fn main() {
// 两次二查找法
// 如果 `target` 在数组中的个数比较多, 这种算法效率很高, `O(log n)`
pub fn search_range3(nums: Vec<i32>, target: i32) -> Vec<i32> {
    fn search_left(nums: &[i32], target: i32) -> i32 {
        let len = nums.len();
        // 极端情况
        if len == 0 || nums[0] > target || nums[len - 1] < target {
            return -1;
        }

        // 极端情况
        if nums[0] == target {
            return 0;
        }

        let mut left = 1;
        let mut right = len - 1;
        let mut low: i32 = -1;
        while left <= right {
            let middle = left + (right - left) / 2;
            match nums[middle].cmp(&target) {
                Ordering::Less => left = middle + 1,
                Ordering::Equal => {
                    low = middle as i32;
                    // 即使当前元素等于 target, 也要调整右侧的索引向左移动
                    right = middle - 1;
                }
                // 这里不需使用 saturating_sub() 防止右侧索引 underflow,
                // 因为 middle >= left >= 1
                Ordering::Greater => right = middle - 1,
            }
        }
        low
    }

    fn search_right(nums: &[i32], target: i32) -> i32 {
        let len = nums.len();
        // 极端情况
        if len == 0 || nums[0] > target || nums[len - 1] < target {
            return -1;
        }

        // 极端情况
        if nums[len - 1] == target {
            return len as i32 - 1;
        }

        let mut left = 0;
        let mut right = len - 2;
        let mut high: i32 = -1;
        while left <= right {
            let middle = left + (right - left) / 2;
            match nums[middle].cmp(&target) {
                Ordering::Less => left = middle + 1,
                Ordering::Equal => {
                    high = middle as i32;
                    // 即使当前元素等于 target, 也要调整左侧索引向右移动
                    left = middle + 1;
                }
                // 这里使用 saturating_sub() 防止右侧索引 underflow
                Ordering::Greater => right = middle.saturating_sub(1),
            }
        }
        high
    }

    vec![search_left(&nums, target), search_right(&nums, target)]
}
}

0035. 搜索插入位置 Search Insert Position

问题描述

这个使用二分查找法.

#![allow(unused)]
fn main() {
use std::cmp::Ordering;

// Binary search
pub fn search_insert1(nums: Vec<i32>, target: i32) -> i32 {
    // 先检查边界情况.
    if nums.is_empty() || target <= nums[0] {
        return 0;
    }
    let last = nums.len() - 1;
    if target > nums[last] {
        return nums.len() as i32;
    }
    if target == nums[last] {
        return last as i32;
    }

    // 左闭右闭区间
    let mut left: usize = 1;
    let mut right: usize = last;

    // 终止循环的条件是 nums[left] > nums[right].
    // 此时 left 所在位置就是 target 插入到数组中的位置.
    while left <= right {
        let middle = left + (right - left) / 2;
        match nums[middle].cmp(&target) {
            Ordering::Less => left = middle + 1,
            Ordering::Equal => return middle as i32,
            // 这里 middle - 1 并不会出现整数 underflow
            Ordering::Greater => right = middle - 1,
        }
    }

    left as i32
}
}

另外, 标准库中的 slice::binary_search() 函数, 也可以查找目标元素 target 的期望位置:

#![allow(unused)]
fn main() {
// 使用 slice::binary_search()
pub fn search_insert2(nums: Vec<i32>, target: i32) -> i32 {
    match nums.binary_search(&target) {
        Ok(index) => index as i32,
        Err(index) => index as i32,
    }
}
}

0039.组合总和 Combination Sum

问题描述

0040. 组合总和 II Combination Sum II

问题描述

TODO(Shaohua):

0042. 接雨水 Trapping Rain Water

问题描述

0046. 全排列 Permutations

问题描述

TODO(Shaohua):

0047. 全排列 II Permutations II

问题描述

0056. 合并区间 Merge Intervals

问题描述

这是一个排序题.

排序

因为给定的区间是无序的, 我们先以每个区间的起始点来对它进行排序, 之后再统计.

步骤如下:

  1. intervals 所有区间的起始点进行排序, intervals.sort_by_key(|interval| interval[0])
  2. 创建一个动态数组 ans, 用来存储不重叠的区间
  3. 遍历 intervals 中的所有数对, 然后合并有重叠的区间, 并将不重叠的区间存储到 ans 数组中
    1. 如果有重叠, 只需要更新区间的终点值即可
    2. 如果没有重叠, 则需要把之间的区间存到 ans 数组, 并同时更新起点和重点
  4. 遍历 ans 数组, 统计所有不重叠区间占用的点数
#![allow(unused)]
fn main() {
// Sorting
pub fn merge1(intervals: Vec<Vec<i32>>) -> Vec<Vec<i32>> {
    debug_assert!(intervals.len() >= 2);
    for interval in &intervals {
        debug_assert!(interval.len() == 2);
    }

    let mut intervals = intervals;
    // 基于起始值来排序.
    intervals.sort_by_key(|interval| interval[0]);

    let mut out = Vec::with_capacity(intervals.len());
    let mut start: i32 = intervals[0][0];
    let mut end: i32 = intervals[0][1];

    for mut interval in intervals {
        let (start1, end1) = (interval[0], interval[1]);
        // 完全没有交叉, 保存上一个值, 并更新 (start, end)
        if start1 > end {
            interval[0] = start;
            interval[1] = end;
            out.push(interval);
            start = start1;
            end = end1;
        } else {
            // 有重叠, 只需要更新 end 的值
            end = end.max(end1);
        }
    }

    // 插入最后一组数值
    out.push(vec![start, end]);
    out
}
}

该算法的时间复杂度是 O(n log(n)), 空间复杂度是 O(n).

0062. 不同路径 Unique Paths

问题描述

加一 Plus One

问题描述

这个题目比较简单, 就是用数组来模拟任意精度的整数相加, 数组中的每个元素代表了十进制整数值中的每个位.

这个问题唯一要操作的是进位置 carry, 从右向左依次将数组中的每个元素与 carry 相加, 然后把结果写回到该元素, 如果产生了新的进位, 就把它写回到 carry.

#![allow(unused)]
fn main() {
pub fn plus_one1(digits: Vec<i32>) -> Vec<i32> {
    let mut digits = digits;
    let mut carry = 1;
    // 从右向左依次遍历数组中的每个进位的值.
    for digit in digits.iter_mut().rev() {
        let sum = *digit + carry;
        carry = sum / 10;
        *digit = sum % 10;
    }
    if carry == 1 {
        digits.insert(0, carry);
    }
    digits
}
}

0067. 二进制求和 Add Binary

问题描述

这个问题考察两个方面的知识:

  • 逆序遍历字符串, 并从中解出每个字符. 这个很适合用迭代器
  • 基本的加法操作, 要注意进位项 (carry), 每个比特位相加时, 都要加上进位项

下图展示的是 a = "1010"; b = "11011" 相加的过程:

add-binary

代码写得就比较自然了, 就按上图描述的:

先构造出两个字符串的迭代器, 用于逆序遍历字符串, 这里为了方面, 我们直接用 a.as_bytes().iter().rev(), 它返回的类型是 Iter<Rev<u8>>.

#![allow(unused)]
fn main() {
use std::iter::Rev;
use std::slice::Iter;

pub fn add_binary1(a: String, b: String) -> String {
    let mut a_iter: Rev<Iter<u8>> = a.as_bytes().iter().rev();
    let mut b_iter = b.as_bytes().iter().rev();
    let mut result = Vec::with_capacity(a_iter.len().max(b_iter.len()) + 1);
    // 进位项
    let mut carry: u8 = 0;

    loop {
        let next_a = a_iter.next();
        let next_b = b_iter.next();
        // 循环的中止条件是, 所有字符串中的比特位都处理完, 且没有进位项.
        if carry == 0 && next_a.is_none() && next_b.is_none() {
            break;
        }

        // 计算当前比特位的值
        let mut bit = carry;
        if let Some(next_a) = next_a {
            bit += next_a - b'0';
        }
        if let Some(next_b) = next_b {
            bit += next_b - b'0';
        }
        // 更新进位项
        carry = bit / 2;
        bit %= 2;
        // 转换成 char 类型
        let bit_char = char::from(bit + b'0');
        result.push(bit_char);
    }

    // 逆序地将比特位转换成字符串
    result.iter().rev().collect()
}
}

x 的平方根 Sqrt(x)

问题描述

这个比较适合二分查找法, 比较容易理解.

#![allow(unused)]
fn main() {
// Binary Search
pub fn my_sqrt1(x: i32) -> i32 {
    assert!(x >= 0);

    if x == 0 || x == 1 {
        return x;
    }

    let mut left: i32 = 0;
    let mut right: i32 = x;
    let mut ans: i32 = 0;

    while left <= right {
        let middle: i32 = left + (right - left) / 2;
        let square = middle.saturating_mul(middle);
        if square > x {
            // 值太大了, 右侧的值向左移
            right = middle - 1;
        } else {
            ans = middle;
            // 有些小, 左侧的值向右移
            left = middle + 1;
        }
    }
    ans
}
}

下面方法是二分查找法的另一个写法, 其边界值的判定条件跟上述方法有所差别:

#![allow(unused)]
fn main() {
// Binary Search
pub fn my_sqrt2(x: i32) -> i32 {
    let mut left = 0;
    let mut right = x;

    while left < right {
        let middle = left + (right - left + 1) / 2;
        let square = middle.saturating_mul(middle);
        if square > x {
            right = middle - 1;
        } else {
            // 注意这里的边界情况.
            left = middle;
        }
    }
    left
}
}

0071.简化路径 Simplify Path

问题描述

简化路径问题有几个点:

  • // 形式, 忽略掉中间的空白
  • /./ 形式, 指向当前目录, 也应该忽略
  • /../ 形式, 跳转到父目录, 应忽略当前目录, 直接转向父目录
  • 其它形式的, 应当保留

可以用 / 字符为分隔符, 把路径分隔成多个部分; 并用栈来存储每一级的目录名. 然后基于上面的规则, 移除不需要的目录名, 最后将栈中所有的字段组装成一个新的路径.

代码实现

Rust

#![allow(unused)]
fn main() {
// Stack
// 优化字符串操作
pub fn simplify_path2(path: String) -> String {
    let mut stack: Vec<&str> = Vec::with_capacity(path.len());
    for part in path.split('/') {
        match part {
            // 忽略空白
            "" => (),
            // 忽略当前目录
            "." => (),
            // 返回上个目录
            ".." => {
                let _ = stack.pop();
            }
            part => stack.push(part),
        }
    }
    // 目录以 "/" 开头
    format!("/{}", stack.join("/"))
}
}

C++

#include <cassert>

#include <sstream>
#include <string>
#include <vector>

std::string simplify_path(std::string path) {
  // 创建栈, 用于存放每个目录名.
  std::vector<std::string> stack;
  std::stringstream ss(path);
  std::string folder_name;
  while (std::getline(ss, folder_name, '/')) {
    if (folder_name.empty()) {
      // 忽略空白
    } else if (folder_name == ".") {
      // 忽略当前目录
    } else if (folder_name == "..") {
      // 返回上个目录
      if (!stack.empty()) {
        stack.pop_back();
      }
    } else {
      stack.emplace_back(folder_name);
    }
  }

  std::string real_path;
  if (stack.empty()) {
    // 目录以 "/" 开头
    real_path += "/";
  } else {
    for (const auto& folder_name : stack) {
      real_path = real_path + "/" + folder_name;
    }
  }

  return real_path;
}

0074. 搜索二维矩阵 Search a 2D Matrix

问题描述

这个题目适合使用二分查找法. 但在解题之外, 我们先试试暴力法.

暴力法

把所有的元素拷贝一遍到新的数组中, 然后使用二分查找法:

#![allow(unused)]
fn main() {
// Brute force
pub fn search_matrix1(matrix: Vec<Vec<i32>>, target: i32) -> bool {
    let items = matrix.len() * matrix[0].len();
    let mut nums = Vec::with_capacity(items);
    for row in matrix {
        nums.extend(row);
    }

    nums.binary_search(&target).is_ok()
}
}

真正运行时, 这个方法其实并不算太慢. 时间复杂度是 O(m n), 空间复杂度是 O(m n).

二分查找法

考虑题目中的条件:

  • 每行数据是递增的
  • 每列数据也是递增的

根据上面的条件, 我们的解决思路是:

  1. 先使用二分查找法遍列第一列, 找到 target 可能位于哪一行
  2. 然后再利用二分查找法遍历该行, 确定 target 是否存在于当前行

但要注意一下细节:

  1. 在查找行时, 我们使用 while top < bottom 的条件判断, 该循环终止的条件是 left == right
  2. 而且计算 middle 值时, 使用的是 let middle = left + (right - left + 1) / 2, 这样的话, 中间值会偏下一位, 但因为之后的条件, bottom = middle - 1, 这样才不会进行死循环
  3. 在查找行数据时, 我们使用 while left <= right 的条件, 该循环的终止条件是, 找到了 target, 或者 left > right 没有找到

代码实现如下:

#![allow(unused)]
fn main() {
// Binary Search
pub fn search_matrix2(matrix: Vec<Vec<i32>>, target: i32) -> bool {
    // 两次二分查找:
    // 1. 确定 target 应该属于哪个行
    // 2. 确定 target 是否在当前行

    debug_assert!(!matrix.is_empty() && !matrix[0].is_empty());

    // 极端情况.
    let last_row = matrix.len() - 1;
    let last_col = matrix[0].len() - 1;
    if target < matrix[0][0] || target > matrix[last_row][last_col] {
        return false;
    }

    let mut top = 0;
    let mut bottom = last_row;
    // 循环终止条件是 top == bottom
    while top < bottom {
        let middle = top + (bottom - top + 1) / 2;
        if matrix[middle][0] > target {
            // target 位于 middle 上面的行
            bottom = middle - 1;
        } else {
            top = middle;
        }
    }
    debug_assert!(top == bottom);
    debug_assert!(matrix[top][0] <= target);
    if top < last_row {
        debug_assert!(target <= matrix[top + 1][0]);
    }

    let mut left = 0;
    let mut right = last_col;
    let row = &matrix[top];
    while left <= right {
        let middle = left + (right - left) / 2;
        match row[middle].cmp(&target) {
            Ordering::Equal => return true,
            Ordering::Less => left = middle + 1,
            Ordering::Greater => right = middle.saturating_sub(1),
        }
    }

    false
}
}

算法的时间复杂度是 O(log(m n)), 空间复杂度是 O(1).

0075. 颜色分类 Sort Colors

问题描述

靠拢型双指针

靠拢型双指针是一种常用方法, 这个解法是它的变体, 叫DNF.

TODO(Shaohua): Add more description.

#![allow(unused)]
fn main() {
// 靠拢型双指针
// Dutch National Flag, DNF
// three-way partition
pub fn sort_colors1(nums: &mut Vec<i32>) {
    assert!(!nums.is_empty());

    // 双指针的变形, 三指针
    // left 用于指向数组中为0的元素的右侧
    let mut left = 0;
    // mid 用于遍历数组
    let mut mid = 0;
    // right 用于指向数组中为2的元素的左侧
    let mut right = nums.len() - 1;

    // 遍历数组
    while mid <= right {
        if nums[mid] == 0 {
            nums.swap(mid, left);
            mid += 1;
            // 左边的指针往右移一下
            left += 1;
        } else if nums[mid] == 2 {
            nums.swap(mid, right);
            // 右边的指针往左移一下
            if right > 0 {
                right -= 1;
            } else {
                break;
            }
        } else {
            mid += 1;
        }
    }
}
}

排序法

各种常见的排序算法都可以, 比如括入排序, 选择排序. 因为这毕竟是一个排序题.

#![allow(unused)]
fn main() {
// 选择排序 Selection Sort
pub fn sort_colors3(nums: &mut Vec<i32>) {
    for i in 0..(nums.len() - 1) {
        for j in i..nums.len() {
            if nums[i] > nums[j] {
                nums.swap(i, j);
            }
        }
    }
}
}

0078. 子集 Subsets

问题描述

TODO(Shaohua):

0080. 删除排序数组中的重复项 II Remove Duplicates from Sorted Array II

问题描述

先分析这个题的条件:

  • 数组是有序的, 有重复元素
  • 原地移除部分重复元素, 每个重复的数值最多只能出现两次
  • 要保持原先的顺序
  • 空间复杂度是 O(1)

快慢型双指针

典型的快慢型双指针问题.

其中, 快指针用于遍历数组; 慢指针用于移除重复元素后, 指向数组的最高位.

fast-slow two-pointers

#![allow(unused)]
fn main() {
// 快慢型双指针
pub fn remove_duplicates1(nums: &mut Vec<i32>) -> i32 {
    assert!(!nums.is_empty());
    let len = nums.len();

    // 慢指针指向结果数组的最高位
    let mut slow = 0;
    // 用快指针遍历数组
    let mut fast = 0;

    // 遍历数组
    while fast < len {
        let curr = nums[fast];
        // 元素重复的次数
        let mut dup = 0;

        // 复制前两个重复的元素, 而忽略后面的.
        while (fast < len) && (curr == nums[fast]) {
            if dup < 2 {
                nums[slow] = curr;
                // 同时移动慢指针, 结果数组最高位 +1
                slow += 1;
            }
            dup += 1;
            fast += 1;
        }
    }

    slow as i32
}
}

优先双指针

这里的优化仅仅是简化了思路, 但是性能并不一定更好.

simplify-two-pointers

#![allow(unused)]
fn main() {
// 优化双指针
pub fn remove_duplicates2(nums: &mut Vec<i32>) -> i32 {
    assert!(!nums.is_empty());
    let len = nums.len();
    if len < 3 {
        return len as i32;
    }

    // 慢指针指向结果数组的最高位, 跳过前两个元素
    let mut slow = 2;

    // 用快指针遍历数组, 跳过前两个元素
    for fast in 2..len {
        // 这里是关键点!
        // `slow - 2` 表示允许有两个重复的元素.
        if nums[fast] != nums[slow - 2] {
            // 移动慢指针
            nums[slow] = nums[fast];
            slow += 1;
        }
    }

    slow as i32
}
}

相关问题

0082. 删除排序链表中的重复元素 II Remove Duplicates from Sorted List II

问题描述

TODO(Shaohua):

0083. 删除排序链表中的重复元素 Remove Duplicates from Sorted List

问题描述

TODO(Shaohua):

0088. 合并两个有序数组 Merge Sorted Array

问题描述

TODO(Shaohua):

0090. 子集 II Subsets II

问题描述

TODO(Shaohua):

问题 0101-0200

0125. 验证回文串 Valid Palindrome

问题描述

靠拢型双指针

这是一个典型的双指针问题, 相关介绍可以看这里.

close-up

#![allow(unused)]
fn main() {
// 靠拢型双指针
pub fn is_palindrome1(s: String) -> bool {
    // 根据题目要求, 过滤一下字符串
    let s: String = s
        .chars()
        .filter(char::is_ascii_alphanumeric)
        .map(|c| c.to_ascii_lowercase())
        .collect();
    if s.is_empty() {
        return true;
    }

    // 使用双指针来判断
    let mut left = 0;
    let mut right = s.len() - 1;
    let bytes = s.as_bytes();
    while left < right {
        if bytes[left] != bytes[right] {
            return false;
        }
        left += 1;
        right -= 1;
    }
    true
}
}

当然也可以不过滤字符, 直接应用双指针:

#![allow(unused)]
fn main() {
// 靠拢型双指针, 但不对字符串预处理
pub fn is_palindrome2(s: String) -> bool {
    let chars: Vec<char> = s.chars().collect();
    if chars.is_empty() {
        return true;
    }

    let mut left = 0;
    let mut right = chars.len() - 1;
    while left < right {
        // 忽略非字符数字
        while left < right && !chars[left].is_ascii_alphanumeric() {
            left += 1;
        }
        // 忽略非字符数字
        while left < right && !chars[right].is_ascii_alphanumeric() {
            right -= 1;
        }

        if chars[left].to_ascii_lowercase() != chars[right].to_ascii_lowercase() {
            return false;
        }
        left += 1;
        right -= 1;
    }
    true
}
}

利用回文的特性

回文的特性就是, 把它反转过来, 会得到一样的结果.

我们只需要反转字符串, 并与原字符串对比一下就能确定, 它是不是回文.

reverse-string

#![allow(unused)]
fn main() {
// 使用回文字串的性质: 反转之后依然相同
pub fn is_palindrome3(s: String) -> bool {
    let s: String = s
        .chars()
        .filter(char::is_ascii_alphanumeric)
        .map(|c| c.to_ascii_lowercase())
        .collect();
    s.chars().rev().collect::<String>() == s
}
}

相关问题

0136. Single Number

问题描述

这个问题考察的是比特位异或操作中的一个重要特性: A XOR A == 0.

我们可以利用这个特性, 遍历数组中的每一项, 然后计算异或值, 最后的结果就是那个单值.

self-xor

这个思路, 可以用于快速消除数组中出现偶数次的元素.

代码也非常简单:

#![allow(unused)]
fn main() {
// num ^ num = 0
pub fn single_number(nums: Vec<i32>) -> i32 {
    let mut ans = 0;
    for num in &nums {
        ans ^= num;
    }
    ans
}
}

0137. Single Number II

问题描述

第一种思路, 使用字典来计数

很直接的想法, 用字典来统计所有数值出现的次数, 然后遍历该字典, 找到次数为1的那个数值.

#![allow(unused)]
fn main() {
use std::collections::HashMap;

// map, brute force
// 使用字典来统计数值出现的次数
pub fn single_number1(nums: Vec<i32>) -> i32 {
    let mut map: HashMap<i32, usize> = HashMap::new();
    for &num in &nums {
        map.entry(num).and_modify(|count| *count += 1).or_insert(1);
    }
    for (num, count) in map {
        if count == 1 {
            return num;
        }
    }
    -1
}
}

第二种思路, 使用 BitVector

这个题目, 因为是奇数次重复的数, 所以不能再使用 0136. Single Number 里面的方法.

这个需要一个新的思路, 那就是 BitVector.

  • 遍历数组中的每个整数的每一个比特位
  • 对所有整数的同一个比特位求和, 然后对3取余, 因为大部分整数都出现了3次; 求得的余数, 就是落单的数值在该比特位的比特值
  • 当遍历完整数的所有比特位后, 就可以计算出落单整数的所有比特位的信息, 也就可以组装出了它的具体数值

基本的操作过程如下图所示:

bit vector

要注意的是, 这个解决方法, 可以解决数组中有奇数个重复的整数, 也可以解决有偶数个重复的整数. 也就是说, 它可以用来解 0136. Single Number.

具体的代码如下:

#![allow(unused)]
fn main() {
// bit vector
pub fn single_number2(nums: Vec<i32>) -> i32 {
    const DIGIT_LEN: usize = 32;

    let mut ans = 0;
    // 遍历所有比特位
    for i in 0..DIGIT_LEN {
        // 计数所有整数在该比特位处的和
        let sum = nums.iter().map(|num| num >> i & 1).sum::<i32>();
        // bit 的值就是落单的数在该比特位处的比特值.
        let bit = sum % 3;
        ans |= bit << i;
    }
    ans
}
}

第三种思路

TODO(Shaohua): Use ones and twos to record.

0150. 逆波兰表达式求值 Evaluate Reverse Polish Notation

问题描述

这个问题是解析后缀表达式, 可以用栈来完成.

但要注意操作符对应的左右两侧的数值顺序.

代码实现

Rust

#![allow(unused)]
fn main() {
// Stack
// 后缀表达式
// - 优化模式匹配
// - 优化出栈操作
pub fn eval_rpn2(tokens: Vec<String>) -> i32 {
    let mut stack: Vec<i32> = Vec::with_capacity(tokens.len());

    for token in tokens.iter() {
        match token.as_str() {
            // 匹配运算符.
            "+" => {
                debug_assert!(stack.len() >= 2);
                let num1 = stack.pop().unwrap();
                let num2 = stack.last_mut().unwrap();
                *num2 += num1;
            }
            "-" => {
                debug_assert!(stack.len() >= 2);
                let num1 = stack.pop().unwrap();
                let num2 = stack.last_mut().unwrap();
                *num2 -= num1;
            }
            "*" => {
                debug_assert!(stack.len() >= 2);
                let num1 = stack.pop().unwrap();
                let num2 = stack.last_mut().unwrap();
                *num2 *= num1;
            }
            "/" => {
                debug_assert!(stack.len() >= 2);
                let num1 = stack.pop().unwrap();
                let num2 = stack.last_mut().unwrap();
                *num2 /= num1;
            }
            num_str => {
                //let num: i32 = i32::from_str(num_str).unwrap();
                //let num: i32 = i32::from_str_radix(num_str, 10).unwrap();
                let num: i32 = num_str.parse().unwrap();
                stack.push(num);
            }
        }
    }
    // 栈顶的元素就是计算结果.
    stack.pop().unwrap()
}
}

C++

#include <cassert>

#include <stack>
#include <string>
#include <vector>

int evalRPN(std::vector<std::string>& tokens) {
  std::stack<int> stack;

  for (const std::string& token : tokens) {
    if (token == "+") {
      const int num1 = stack.top();
      stack.pop();
      const int num2 = stack.top();
      stack.pop();
      const int num = num2 + num1;
      stack.push(num);
    } else if (token == "-") {
      const int num1 = stack.top();
      stack.pop();
      const int num2 = stack.top();
      stack.pop();
      const int num = num2 - num1;
      stack.push(num);
    } else if (token == "*") {
      const int num1 = stack.top();
      stack.pop();
      const int num2 = stack.top();
      stack.pop();
      const int num = num2 * num1;
      stack.push(num);
    } else if (token == "/") {
      const int num1 = stack.top();
      stack.pop();
      const int num2 = stack.top();
      stack.pop();
      const int num = num2 / num1;
      stack.push(num);
    } else {
      const int num = std::stoi(token);
      stack.push(num);
    }
  }

  // 栈顶的元素就是计算结果.
  return stack.top();
}

void check_solution() {
  std::vector<std::string> tokens = {"4","13","5","/","+"};
  const int ret = evalRPN(tokens);
  assert(ret == 6);
}

int main() {
  check_solution();
  return 0;
}

寻找旋转排序数组中的最小值 Find Minimum in Rotated Sorted Array

问题描述

这个是二分查找法的变体.

先分析题目中的条件:

  1. 数组中没有重复的元素
  2. 数组最初是升序排序的
  3. 数组中的元素被右移了 k

基于以上条件, 我们可以推出, 数组中元素可能的布局只有三种情况:

  • (1, 2, 3)
  • (3, 1, 2)
  • (2, 3, 1)

以上的布局决定了二分查找法中的 middle 元素所在的趋势. 大概如下图所示:

number orders

有了这样的分析, 就可以编写二分查找算法了:

#![allow(unused)]
fn main() {
// Binary Search
pub fn find_min1(nums: Vec<i32>) -> i32 {
    assert!(!nums.is_empty());

    // 极限情况.
    let last = nums.len() - 1;
    if nums[0] < nums[last] {
        return nums[0];
    }

    let mut left = 0;
    let mut right = last;

    while left < right {
        let middle = left + (right - left) / 2;
        // (1, 2, 3)
        if nums[left] < nums[middle] && nums[middle] < nums[right] {
            // 最小值
            return nums[left];
        }

        if nums[middle] > nums[right] {
            // (2, 3, 1)
            // 右侧部分较小
            left = middle + 1;
        } else {
            // (3, 1, 2)
            // 左侧部分较小
            right = middle;
        }
    }
    nums[left]
}
}

0154. 寻找旋转排序数组中的最小值 II Find Minimum in Rotated Sorted Array II

问题描述

这个问题是 0153 的扩展. 这个表面上看起来可以直用用二分查找法, 但因为可以有相同值的元素存在, 它把问题复杂化了.

暴力法 Brute force

遍历数组, 计算最小值:

#![allow(unused)]
fn main() {
// Brute force
pub fn find_min1(nums: Vec<i32>) -> i32 {
    nums.iter().min().copied().unwrap_or_default()
}
}

时间复杂度是 O(n).

二分查找法

这个要对二分查找法的命中条件做一些改进. 因为有重复元素的存在, 我们在某些条件下无法确定元素的顺序, 但可以只对有明确顺序的情况使用二分查找:

  • 如果 nums[middle] < nums[right] 则最小值不在右侧部分
  • 如果 nums[middle] > nums[right] 则最小值在右侧部分
  • 其它情况, 一次将 left 右移一步, 将 right 左移一步, 渐渐靠近
#![allow(unused)]
fn main() {
// Binary Search
#[allow(clippy::comparison_chain)]
pub fn find_min2(nums: Vec<i32>) -> i32 {
    assert!(!nums.is_empty());

    let mut left = 0;
    let mut right = nums.len() - 1;
    while left + 1 < right {
        let middle = left + (right - left) / 2;
        //println!("left: {left}, middle: {middle} right: {right}, nums: {nums:?}");
        if nums[middle] < nums[right] {
            // (1, 2, 3)
            // 最小值位于左侧
            right = middle;
        } else if nums[middle] > nums[right] {
            // (2, 3, 1)
            // 最小值位于右侧
            left = middle + 1;
        } else {
            // 不容易确定, 一次移动一个位置, 考虑重复的元素.
            if right > left && nums[right - 1] <= nums[right] {
                right -= 1;
            }
            if left < right && nums[left + 1] <= nums[left] {
                left += 1;
            }
        }
    }

    nums[left].min(nums[right])
}
}

该算法的时间复杂度是 O(log(n)).

0155. 最小栈 Min Stack

问题描述

这个问题可以有两种解法:

  • 使用两个栈, 分别存放正常的数值, 和当前位置的最小值
  • 使用一个栈, 但是栈中每个元素是一个元组, 分别存储 当前值和最小值

使用两个栈

Rust

#![allow(unused)]
fn main() {
// 用两个栈来实现
#[derive(Debug, Clone)]
pub struct MinStack {
    // 正常的栈, 按入栈顺序存储.
    stack: Vec<i32>,
    // 最小栈, 里面存储当前位置的最小元素, 最小元素可能是有重复的,
    // 其长度与 `stack` 相同.
    min_stack: Vec<i32>,
}

impl Default for MinStack {
    fn default() -> Self {
        Self::new()
    }
}

impl MinStack {
    #[must_use]
    #[inline]
    pub const fn new() -> Self {
        Self {
            stack: Vec::new(),

            min_stack: Vec::new(),
        }
    }

    pub fn push(&mut self, val: i32) {
        // 将元素入栈
        self.stack.push(val);
        if let Some(top) = self.min_stack.last() {
            // 保存当前位置最小的元素到 min_stack.
            self.min_stack.push(*top.min(&val));
        } else {
            self.min_stack.push(val);
        }
    }

    pub fn pop(&mut self) {
        let _ = self.stack.pop();
        let _ = self.min_stack.pop();
    }

    #[must_use]
    pub fn top(&self) -> i32 {
        self.stack.last().copied().unwrap_or_default()
    }

    #[must_use]
    pub fn get_min(&self) -> i32 {
        self.min_stack.last().copied().unwrap_or_default()
    }

    #[must_use]
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.stack.is_empty()
    }
}
}

C++

#include <cassert>

#include <stack>

class MinStack {
 public:
  MinStack() {}

  void push(int val) {
    this->stack_.push(val);
    if (this->min_stack_.empty()) {
      this->min_stack_.push(val);
    } else {
      const int current_min = this->min_stack_.top();
      const int new_min = std::min(current_min, val);
      this->min_stack_.push(new_min);
    }
  }

  void pop() {
    this->stack_.pop();
    this->min_stack_.pop();
  }

  int top() {
    return this->stack_.top();
  }

  int getMin() {
    return this->min_stack_.top();
  }

 private:
  std::stack<int> stack_;
  std::stack<int> min_stack_;
};

使用一个栈

Rust

#![allow(unused)]
fn main() {
// 用一个栈来实现
#[derive(Debug, Clone)]
pub struct MinStack {
    // 元组: (当前的元素, 当前最小的元素)
    stack: Vec<(i32, i32)>,
}

impl Default for MinStack {
    fn default() -> Self {
        Self::new()
    }
}

impl MinStack {
    #[must_use]
    #[inline]
    pub const fn new() -> Self {
        Self { stack: Vec::new() }
    }

    pub fn push(&mut self, val: i32) {
        if let Some(top) = self.stack.last() {
            let min = top.1.min(val);
            self.stack.push((val, min));
        } else {
            self.stack.push((val, val));
        }
    }

    pub fn pop(&mut self) {
        let _ = self.stack.pop().unwrap();
    }

    #[must_use]
    pub fn top(&self) -> i32 {
        self.stack.last().unwrap().0
    }

    #[must_use]
    pub fn get_min(&self) -> i32 {
        self.stack.last().unwrap().1
    }

    #[must_use]
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.stack.is_empty()
    }
}
}

C++

#include <cassert>

#include <stack>

class MinStack {
 public:
  MinStack() {}

  void push(int val) {
    if (this->stack_.empty()) {
      this->stack_.emplace(val, val);
    } else {
      const int current_min = this->getMin();
      const int new_min = std::min(current_min, val);
      this->stack_.emplace(val, new_min);
    }
  }

  void pop() {
    this->stack_.pop();
  }

  int top() {
    return this->stack_.top().first;
  }

  int getMin() {
    return this->stack_.top().second;
  }

 private:
  std::stack<std::pair<int, int>> stack_;
};

int main() {
  return 0;
}

0162. 寻找峰值 Find Peak Element

问题描述

这个题目中的数组并不是有序的, 所以可以先考虑暴力法.

Brute force

遍历整个数组, 找到比左侧和右侧都大的那个元素.

但首先要检查边界情况, 即第一个元素和最后一个元素.

#![allow(unused)]
fn main() {
// Brute Force
// O(n)
pub fn find_peak_element1(nums: Vec<i32>) -> i32 {
    debug_assert!(!nums.is_empty());
    if nums.len() == 1 {
        return 0;
    }
    // 先处理边界情况.
    if nums[0] > nums[1] {
        return 0;
    }
    let last = nums.len() - 1;
    if nums[last - 1] < nums[last] {
        return last as i32;
    }

    // 检查剩下的元素.
    for i in 1..last {
        if nums[i - 1] < nums[i] && nums[i] > nums[i + 1] {
            return i as i32;
        }
    }

    -1
}
}

该算法的时间复杂度是 O(n).

二分查找法

这个表面上看, 并不能直接使用二分查找法. 但是, 这个题目只要求找到极大值, 并没有要求找到数组中的最大值, 所以仍然可以用二分查找法找出 比左侧和右侧都大的元素.

二分法中的 middle 元素与可能的极大值的关系有三种:

  • middle 处就是峰值
  • middle 位于峰值的左侧
  • middle 位于峰值的右侧

peak position

我们编写二分查找法时, 就可以根据这些情况来处理:

#![allow(unused)]
fn main() {
// Binary Search
pub fn find_peak_element2(nums: Vec<i32>) -> i32 {
    debug_assert!(!nums.is_empty());

    if nums.len() == 1 {
        return 0;
    }

    // 先处理边界情况.
    // 这里先检查第一个和最后一个元素.
    if nums[0] > nums[1] {
        return 0;
    }
    let last = nums.len() - 1;
    if nums[last - 1] < nums[last] {
        return last as i32;
    }

    // 使用二分法找一个峰值, 检查数组中剩下的元素.
    let mut left = 1;
    let mut right = last - 1;
    while left <= right {
        // 峰值出现的位置与 nums[middle] 的关系有三种情况.
        let middle = left + (right - left) / 2;
        if nums[middle] > nums[middle + 1] && middle > 0 && nums[middle] > nums[middle - 1] {
            // 1. middle 处就是峰值
            return middle as i32;
        }
        if nums[middle] < nums[middle + 1] {
            // 2. 峰值在 middle 的右侧
            left = middle + 1;
        } else if middle > 0 && nums[middle] < nums[middle - 1] {
            // 3. 峰值在 middle 的左侧
            right = middle - 1;
        }
    }

    -1
}
}

该算法的时间复杂度是 O(log(n)).

两数之和 II - 输入有序数组 Two Sum II - Input Array Is Sorted

问题描述

靠拢型双指针

典型的双指针问题, 就不过多介绍了, 详细的分析看这里.

#![allow(unused)]
fn main() {
// 靠拢型双指针
pub fn two_sum1(numbers: Vec<i32>, target: i32) -> Vec<i32> {
    assert!(numbers.len() >= 2);

    let mut left = 0;
    let mut right = numbers.len() - 1;

    while left < right {
        // 判定条件就其和为0
        let sum = numbers[left] + numbers[right];
        match sum.cmp(&target) {
            Ordering::Less => left += 1,
            Ordering::Greater => right -= 1,
            Ordering::Equal => {
                return vec![left as i32 + 1, right as i32 + 1];
            }
        }
    }

    Vec::new()
}
}

二分查找法

因为数组已经是排好序的了, 也可以先遍历数组, 并用二分查找法找到其和为 target 的对应的元素.

要注意的是, 二分查找法对于有很多的重复元素时, 需要做一下优化, 我们在这里并没有手动实现二分查找法, 而只是调用了 Rust 的 slice::binary_search() 方法.

#![allow(unused)]
fn main() {
// 二分查找法
pub fn two_sum2(numbers: Vec<i32>, target: i32) -> Vec<i32> {
    for (index, &num) in numbers.iter().enumerate() {
        // 从下个元素开始使用二分查找法, 搜索对应的元素.
        if let Ok(slice_index) = numbers[index + 1..].binary_search(&(target - num)) {
            // 更新索引值.
            let next_index = slice_index + index + 1;
            return vec![index as i32 + 1, next_index as i32 + 1];
        }
    }

    Vec::new()
}

旋转数组 Rotate Array

问题描述

三次反转法

操作过程如下:

  1. arr[k..n] 进行反转
  2. arr[0..k] 进行反转
  3. arr[..] 进行反转

这个方法是在原地操作的, 其时间复杂度是 O(n), 空间复杂度是 O(1).

#![allow(unused)]
fn main() {
/// 三次反转法
pub fn rotate2(nums: &mut Vec<i32>, k: i32) {
    // 检查边界条件
    if nums.is_empty() || k <= 0 {
        return;
    }
    let len: usize = nums.len();
    let k: usize = (k as usize) % len;
    if k == 0 {
        return;
    }

    // 第一步, 把所有元素做反转.
    nums.reverse();
    // 第二步, 找到右移的分界线 k, 把 [0..k] 做反转.
    nums[0..k].reverse();
    // 第三步, 把 [k..len] 做反转
    nums[k..].reverse();
}
}

使用时时数组

操作过程如下:

  1. 创建辅助数组
  2. arr[(len - k)..] 复制到辅助数组
  3. arr[..(len - k)] 复制到辅助数组
  4. 将辅助数组中的内容与目标数组交换, 通过 mem::swap()

这个方法不是在原地操作的, 其时间复杂度是 O(n), 空间复杂度是 O(n).

#![allow(unused)]
fn main() {
/// 使用临时数组
pub fn rotate3(nums: &mut Vec<i32>, k: i32) {
    if nums.is_empty() || k <= 0 {
        return;
    }
    let len = nums.len();
    let k = len - (k as usize) % len;
    if k == 0 {
        return;
    }

    let mut aux = Vec::with_capacity(len);
    aux.extend_from_slice(&nums[k..]);
    aux.extend_from_slice(&nums[..k]);
    mem::swap(nums, &mut aux);
}
}

参考

0191. Number of 1 Bits

问题描述

这个题目考察位操作的.

先考虑比特位操作的函数表:

ABA OR BA AND BA XOR BNOT A
111100
101010
011011
000001

其次是如何遍历 u32 的所有比特位? 是的, 用右移 (shift right), 依次将每一个比特位右移到最低位, 然后结合上面的逻辑与操作(bitwise AND), 就可以取出这个比特位的值, 是 0 或者 1.

下图展示的是 n=11 (即 0b1011) 时的右移操作:

walk-through u32

想明白了上面的过程, 代码就比较容易了.

#![allow(unused)]
fn main() {
pub fn hamming_weight3(n: i32) -> i32 {
    let mut count = 0;
    for i in 0..32 {
        count += n >> i & 1;
    }
    count
}
}

或者使用函数式风格的写法:

#![allow(unused)]
fn main() {
pub fn hamming_weight4(n: i32) -> i32 {
    (0..32).map(|i| n >> i & 1).sum()
}
}

问题 0201-0300

0217. 存在重复元素 Contains Duplicate

问题描述

方法1, Brute Force

直接暴力遍历 vector, 查找当前元素是否在后面出现过.

brute-force

这个方法的时间复杂度为 O(n^2). 可想而知, 这个解法是无法通过 leetcode 平台检验的, 结果会超时. 这个方法可以用来查找具体哪些元素是重复的.

#![allow(unused)]
fn main() {
// 两层遍历数组
pub fn contains_duplicate(nums: Vec<i32>) -> bool {
    assert!(!nums.is_empty());
    let len = nums.len();
    for i in 0..(len - 1) {
        for j in (i + 1)..len {
            if nums[i] == nums[j] {
                return true;
            }
        }
    }
    false
}
}

方法2, 先排序, 再遍历

既然暴力遍历比较慢, 那能不能加快一些? 如果我们先给 vector 做排序, 然后应该只需要遍历它一次, 每个元素与相邻的元素比较是否相等, 就可以判定 是否包含重复的元素.

sort-first

这个方法的时间复杂度为 O(nlogn), 它还可以用来查找具体哪些元素是重复的.

#![allow(unused)]
fn main() {
// 先排序, 再遍历
pub fn contains_duplicate2(nums: Vec<i32>) -> bool {
    assert!(!nums.is_empty());
    let mut nums = nums;
    // 先排序
    nums.sort();
    let len = nums.len();
    for i in 0..(len - 1) {
        if nums[i] == nums[i + 1] {
            return true;
        }
    }
    false
}
}

方法3, 使用 HashSet 缓存数值

考虑到集合 (HashSet) 快速能查找元素的特性(时间是O(1)), 我们可以用它来存储元素, 加快查找.

先遍历整个数组, 查找集合中是否存在该元素, 如果存在就返回, 如果不存在就把它插入到集合中.

hash-set

这个方法的时间复杂度为 O(nlogn), 空间复杂度为 O(n), 它还可以用来查找具体哪些元素是重复的.

#![allow(unused)]
fn main() {
// 使用 Hash Set 存放访问过的数组中的元素.
pub fn contains_duplicate3(nums: Vec<i32>) -> bool {
    if nums.len() < 2 {
        return false;
    }
    let mut cache = HashSet::with_capacity(nums.len());
    for &num in nums.iter() {
        // 如果 insert() 返回 false, 就说明 cache 之前已经包含 num 了; 说明 num 是重复的元素.
        if !cache.insert(num) {
            return true;
        }
    }
    false
}
}

方法4, 使用 HashSet 缓存所有数值

上面的方法3已经足够好了. 但是, 我们仔细考虑题目, 发现它并不要求我们找到究竟是哪些元素是重复的.

基于此, 我们可以对方法3做一下修改:

  • 遍历整个数组, 将里面的所有元素都插入到集合中
  • 比较集合中元素的个数是否与数组中元素的个数相同, 如果不同, 那就说明有重复元素

hash-set

这个方法是利用了 HashSet 不存储重复元素的特性. 它的时间复杂度也是 O(nlogn). 要注意的是, 这个方法并不一定比方法3更快, 这个得看数组中是否有重复元素, 以及重复元素所在位置.

#![allow(unused)]
fn main() {
// 使用 Hash Set 存放数组中的所有元素, 最后只比较两者中元素的个数是否相同.
pub fn contains_duplicate4(nums: Vec<i32>) -> bool {
    let set = nums.iter().collect::<HashSet<_>>();
    set.len() != nums.len()
}
}

0219. 存在重复元素II Contains Duplicate II

问题描述

这个问题与 0001. 两数之和 Two Sum 很相似, 而且其解法也都是一样的.

方法1, Brute Force

这个方法比较直接, 就是遍历数组, 并遍历后面的每个元素, 判断它们是否重复.

brute-force

因为有两层遍历, 这个方法的时间复杂度是 O(n^2).

#![allow(unused)]
fn main() {
// Brute Force
pub fn contains_nearby_duplicate1(nums: Vec<i32>, k: i32) -> bool {
    let len = nums.len();
    let k = k as usize;

    for i in 0..(len - 1) {
        for j in (i + 1)..len {
            if nums[i] == nums[j] && j - i <= k {
                return true;
            }
        }
    }
    false
}
}

方法2, 哈稀表

同样是需要遍历整个数组, 我们可以使用哈稀表缓存一下访问过的元素, 以加快查找元素的时间. 这个哈稀表用于记录元素值到它在数组中的索引值之间的关系.

hash-table

这个方法的时间复杂度是 O(nlogn).

#![allow(unused)]
fn main() {
// 使用 HashMap
pub fn contains_nearby_duplicate2(nums: Vec<i32>, k: i32) -> bool {
    let k = k as usize;

    // map 用于存储数值及其在数组中的位置
    let mut map = HashMap::<i32, usize>::with_capacity(nums.len());

    // 遍历数组中所有元素
    for (index, &num) in nums.iter().enumerate() {
        // 在 map 中尝试查找这个元素, 并判断与当前遍历的元素是否重复.
        if let Some(&old_index) = map.get(&num) {
            if (index - old_index) <= k {
                return true;
            }
        }
        map.insert(num, index);
    }
    false
}
}

0231. 2的幂 Power of Two

问题描述

所有小于1的整数, 都不是2的次幂, 这样就可以过滤到一半的整数了.

方法1, 暴力法

想法很直接, 2^x 数值的个数很有限, 依次生成它们, 然后跟目标整数作一下比对, 看是否相等.

          1 = 0b1
          2 = 0b10
          4 = 0b100
          8 = 0b1000
         16 = 0b10000
         32 = 0b100000
         64 = 0b1000000
        128 = 0b10000000
        256 = 0b100000000
        512 = 0b1000000000
       1024 = 0b10000000000
       2048 = 0b100000000000
       4096 = 0b1000000000000
       8192 = 0b10000000000000
      16384 = 0b100000000000000
      32768 = 0b1000000000000000
      65536 = 0b10000000000000000
     131072 = 0b100000000000000000
     262144 = 0b1000000000000000000
     524288 = 0b10000000000000000000
    1048576 = 0b100000000000000000000
    2097152 = 0b1000000000000000000000
    4194304 = 0b10000000000000000000000
    8388608 = 0b100000000000000000000000
   16777216 = 0b1000000000000000000000000
   33554432 = 0b10000000000000000000000000
   67108864 = 0b100000000000000000000000000
  134217728 = 0b1000000000000000000000000000
  268435456 = 0b10000000000000000000000000000
  536870912 = 0b100000000000000000000000000000
 1073741824 = 0b1000000000000000000000000000000

这个方法要注意处理好边角问题, 先判断整数是否小于1.

#![allow(unused)]
fn main() {
pub fn is_power_of_two1(n: i32) -> bool {
    if n <= 0 {
        return false;
    }
    if n == 1 {
        return true;
    }

    let mut power = 1;
    while 0 < power && power < n {
        power <<= 1;
        if power == n {
            return true;
        }
    }
    false
}
}

方法2, 统计比特位为1的个数

仔细看方法1的说明, 可以发现, 2^x 的比特位中只能有一个是1, 其它都是0.

这是一个重要的2的次幂的特性, 利用这个特性, 可以很快速地判断整数是否为2的次幂.

#![allow(unused)]
fn main() {
// Count ones
pub fn is_power_of_two2(n: i32) -> bool {
    if n <= 0 {
        return false;
    }
    n.count_ones() == 1
}
}

相关问题

0234. 回文链表 Palindrome Linked List

问题描述

TODO(shaohua)

除自身以外数组的乘积 Product of Array Except Self

问题描述

Brute force

这个方法会超时, 但是能帮助我们理解问题.

解决方法也很直接:

  • 初始化目标数组, 其中每个元素都是 1
  • 遍历目标数组, 并计算在当前位置的最终乘积, 通过依次遍历原数组

代码实现如下:

#![allow(unused)]
fn main() {
// Brute force
// 超时
pub fn product_except_self1(nums: Vec<i32>) -> Vec<i32> {
    let len = nums.len();
    let mut res = vec![0; len];
    for (i, product) in res.iter_mut().enumerate() {
        let mut prod = 1;
        for (j, num) in nums.iter().enumerate() {
            if i == j {} else {
                prod *= num;
            }
        }
        *product = prod;
    }

    res
}
}

该算法的时间复杂度是 O(n^2), 空间复杂度是 O(1).

使用除法

题目中禁止使用除法, 但并不妨碍我们尝试一下.

这个算法的步骤是:

  • 遍历数组, 计算所有元的积, 并计算其中为0的元数个数
    • 如果当前元素的值为0, 将0的计数加1
    • 如果当前元素的值不为0, 就把它更新到所有元素的积
  • 再次遍历原数组, 判断当前元素的值, 来计算当前索引位置的结果
    • 如果为0, 整个数组中0的个数为1, 说明其它元素都不为0, 该位置的值是 整个数组的积 / 当前元素的值
    • 如果为0, 则该位置的值是0
    • 如果不为0, 并且整个数组中0的个数不为0, 则该位置的值是0
    • 其它情况, 该位置的值是 整个数组的积 / 当前元素的值

该算法的实现如下:

#![allow(unused)]
fn main() {
// 不使用 Prefix Sum, 但是使用除法
// 时间: O(n), 空间: O(1)
// 可以重用原有的数组
pub fn product_except_self2(nums: Vec<i32>) -> Vec<i32> {
    let mut nums = nums;
    let mut product: i32 = 1;
    let mut num_zeros: usize = 0;
    for &num in &nums {
        if num == 0 {
            num_zeros += 1;
        } else {
            product *= num;
        }
    }

    for num in nums.iter_mut() {
        match (*num, num_zeros) {
            (0, 1) => *num = product,
            (0, _) => *num = 0,
            (_, num_zeros) if num_zeros > 0 => *num = 0,
            (_, _) => *num = product / *num,
        }
    }
    nums
}
}

这种算法的特点:

  • 时间复杂度是 O(n)
  • 空间复杂度是 O(1)
  • 除法的性能要差一些
  • 需要更全面的考虑当前元素是否为0, 以及原数组中0的个数, 这个逻辑有些复杂
  • 它可以重用原数组来存储返回结果

前缀和 Prefix sum

这个类似于计算数组的前缀和数组 (prefix sum array), 我称之为使用前缀积, 本选购就是以空间换时间. 通过将中间结果存储下来, 来减少每个元素位置使用的计算次数.

这种算法的步骤如下:

  • 初始化结果数组, 数组中每个元素的值都为1
  • 从左到右遍历原数组, 并累积计算元素的乘积, 将乘积保存到目标数组中的相同位置
  • 从右到左遍历原数组, 并累积计算元素的乘积, 目标数组中的相同位置的元素与该乘积结果相乘, 就是该元素的最终值

prefix product

代码实现如下:

#![allow(unused)]
fn main() {
// 前缀和 Prefix Sum
// 前缀积与后缀积 Prefix Product & Suffix Product
pub fn product_except_self3(nums: Vec<i32>) -> Vec<i32> {
    let len = nums.len();
    let mut res: Vec<i32> = vec![1; len];
    let mut product = 1;

    // 计算前缀的积
    for (i, num) in nums.iter().enumerate() {
        res[i] *= product;
        product *= num;
    }

    // 乘以后缀的积
    product = 1;
    for i in (0..nums.len()).rev() {
        res[i] *= product;
        product *= nums[i];
    }

    res
}
}

该算法的特点是:

  • 时间复杂度是 O(n)
  • 空间复杂度是 O(1)
  • 没有使用除法

0240. Search a 2D Matrix II Search a 2D Matrix II

问题描述

这个题目中, 二维数组的行和列都是有序的.

暴力法

首先考虑的就是直接将二维数组中的元素排序, 我们通过将所有数值移到同一个数组中, 然后再给它排序, 再利用二分查找法, 这样就比较方便了.

#![allow(unused)]
fn main() {
// Brute force
pub fn search_matrix1(matrix: Vec<Vec<i32>>, target: i32) -> bool {
    let items = matrix.len() * matrix[0].len();
    let mut list = Vec::with_capacity(items);
    for row in matrix {
        list.extend(row);
    }

    list.sort_unstable();

    list.binary_search(&target).is_ok()
}
}

算法的时间复杂度是 O(n log(n)), 空间复杂度是 O(n), 其中 n 是二维数组中的所有元素个数.

另外, 也可以直接使用 HashSet 来存储所有的整数, 它自动排序.

#![allow(unused)]
fn main() {
use std::collections::HashSet;

// Brute force
pub fn search_matrix2(matrix: Vec<Vec<i32>>, target: i32) -> bool {
    let mut set = HashSet::new();
    for row in matrix {
        set.extend(row);
    }

    set.contains(&target)
}
}

对角线法

对于有序的二维数组, 如果我们从数组的右上角开始查找 target, 有三种情况:

  1. 创建 row_indexcol_index, 用于定位二维数组中的元素; 并在开始时将它定位到数组右上角
  2. 当前元素等于 target, 就直接返回
  3. 当前元素比 target 大, 那我们就将 col_index -= 1, 向左侧继续查找
  4. 当前元素比 target 小, 那我们就将 row_index += 1, 去下一行继续查找
  5. 直找我们遍历到二维数组的左下角终止循环, 此时 row_index = matrix_rows - 1, col_index = 0
#![allow(unused)]
fn main() {
use std::cmp::Ordering;

pub fn search_matrix3(matrix: Vec<Vec<i32>>, target: i32) -> bool {
    debug_assert!(!matrix.is_empty());
    debug_assert!(!matrix[0].is_empty());

    // 从右上角开始找
    // 如果当前元素比 target 大, 就向左找更小的元素
    // 如果当前元素比 target 小, 就向下找更大的元素
    // 如果找完了所有空间, 还没找到, 就说明不存在
    let rows = matrix.len();
    let cols = matrix[0].len();

    // 从右上角开始找
    let mut row_index = 0;
    let mut col_index = cols - 1;

    // 循环终止的条件是达到左下角
    while row_index < rows {
        match matrix[row_index][col_index].cmp(&target) {
            Ordering::Equal => return true,
            // 向下找
            Ordering::Less => row_index += 1,
            // 向左找
            Ordering::Greater => {
                if col_index == 0 {
                    break;
                } else {
                    col_index -= 1;
                }
            }
        }
    }

    false
}
}

TODO: 更新时间复杂度

二分查找法

TODO:

第一个错误的版本 First Bad Version

问题描述

这个问题是很简单的二分查找问题.

要查找顺序序列中的一个分界值, 分界值的左侧都是正常版本, 而右侧都是有问题的版本.

要注意二分的边界情况:

#![allow(unused)]
fn main() {
    // Binary Search
    pub fn first_bad_version1(&self, n: i32) -> i32 {
        debug_assert!(n >= 1);
        let mut left = 1;
        let mut right = n;

        while left <= right {
            let middle = left + (right - left) / 2;
            if self.isBadVersion(middle) {
                // [middle..right] 区间都是有问题的版本, 但是 middle - 1 则不确定是不是坏了的.
                right = middle;
            } else {
                // [left..middle] 区间都是没有问题的版本
                left = middle + 1;
            }
        }
        left
    }
}

时间复杂度是 O(log(n)), 空间复杂度是 O(1).

0287. 寻找重复数 Find the Duplicate Number

问题描述

统计字典

  • 创建一个字典, 用于统计每个整数出现的次数
  • 遍历数组, 并更新字典
  • 遍历字典, 找出出现次数大于1的整数, 这个就是重复的数
  • 如果没有重复的整数, 就返回 -1
#![allow(unused)]
fn main() {
use std::collections::HashMap;

// 使用哈稀表计数器
// 时间复杂度: O(n)
// 空间复杂度: O(n)
pub fn find_duplicate1(nums: Vec<i32>) -> i32 {
    let len: i32 = nums.len() as i32;
    assert!(len >= 2);
    let n: i32 = len - 1;
    assert!(n >= 1);
    for &num in &nums {
        assert!(num >= 1 && num <= n);
    }

    let mut map: HashMap<i32, usize> = HashMap::new();
    for &num in &nums {
        map.entry(num).and_modify(|count| *count += 1).or_insert(1);
    }
    for (key, count) in map {
        if count > 1 {
            return key;
        }
    }
    -1
}
}

这个方法的时间复杂度是 O(n), 空间复杂度是 O(n).

集合

这个方法是对上述方法的优化:

  • 创建一个集合, 用于记录整数值是否出现过
  • 遍历数组, 并将它存入到集合中, 如果此时集合中已经存在同样的整数, 这个整数就是重复的
  • 否则返回 -1
#![allow(unused)]
fn main() {
use std::collections::HashSet;

// 使用 HashSet 来记录整数是否被插入过
// 时间复杂度: O(n)
// 空间复杂度: O(n)
pub fn find_duplicate11(nums: Vec<i32>) -> i32 {
    let mut set: HashSet<i32> = HashSet::with_capacity(nums.len());
    for &num in &nums {
        // 如果元素已经在集合中, 就返回false; 如果是第一次插入, 就返回 true.
        if !set.insert(num) {
            return num;
        }
    }
    -1
}
}

这个方法的时间复杂度是 O(n), 空间复杂度是 O(n).

BitSet

使用 bitset 来存储整数值是否出现过.

#![allow(unused)]
fn main() {
// BitSet
// 利用标志位来标记出现次数多于一次的整数.
// 时间复杂度: O(n)
// 空间复杂度: O(n)
pub fn find_duplicate2(nums: Vec<i32>) -> i32 {
    let mut bits: Vec<bool> = vec![false; nums.len()];
    for num in nums {
        let num_usize = num as usize;
        if bits[num_usize] {
            return num;
        }
        bits[num_usize] = true;
    }
    -1
}
}

这个方法的时间复杂度是 O(n), 空间复杂度是 O(n).

这个方法比较浪费空间, 因为每个索引位置占了一个字节. 我们可以改进一下它, 实现一个节省空间的 bitset, 每个索引位置只占一个比特:

#![allow(unused)]
fn main() {
// BitSet
// 利用标志位来标记出现次数多于一次的整数.
// 使用真正的bitset, 而不是 Vec<bool>.
// 时间复杂度: O(n)
// 空间复杂度: O(n)
pub fn find_duplicate22(nums: Vec<i32>) -> i32 {
    let mut bits = [0u64; (100_000) / 64 + 1];

    for num in nums {
        let num_usize = num as usize;
        let slot = num_usize / 64;
        let pos = num_usize % 64;

        let mask = 1 << pos;
        // 检查特定的比特位.
        if bits[slot] & mask == mask {
            return num;
        }
        bits[slot] |= mask;
    }
    -1
}
}

上述几个方法的思路都类似, 只是用于存放整数索引的方式不同.

暴力法 Brute force

  1. 先对数组进行排序 (这里已经不符合题目要求了)
  2. 然后遍历数组, 找到左右相邻元素之间有重复的
#![allow(unused)]
fn main() {
// 先对数组排序, 然后遍历它, 找到重复元素
// 时间复杂度: O(n log(n))
// 空间复杂度: O(n)
// 但是会修改原数组.
pub fn find_duplicate5(nums: Vec<i32>) -> i32 {
    let mut nums = nums;
    nums.sort();

    let len = nums.len();
    for i in 0..(len - 1) {
        if nums[i] == nums[i + 1] {
            return nums[i];
        }
    }
    -1
}
}

因为使用了排序算法, 这个方法比较慢, 时间复杂度是 O(n log(n)), 空间复杂度是 O(1)

使用负数来标记该整数已经出现过

这个方法也会修改数组的值, 其核心思想是利用整数的符号位来标记整数是否存在. 其步骤如下:

  1. 遍历数组, 计数当前元素 m 的绝对值
  2. 以该绝对值作为元素的索引, 找到数组中对应的元素 k
  3. 如果元素 k 本身是一个正数, 就把它转成对应的负数
  4. 如果元素 k 是一个负数, 说明之前就有相同的整数值 m 存在过, 就返回它

下面以 [1, 2, 3, 3, 4] 数组为例展示整个过程:

negative as index

当遍历到 i = 3 时, nums[3] == -3, 说明数组的左侧部分就有整数 3 存在过, 这个整数就是重复的.

相应的代码实现如下:

#![allow(unused)]
fn main() {
// 使用正负数来标记该整数已经出现过
// 时间复杂度: O(n)
// 空间复杂度: O(1)
// 但是它需要修改数组
pub fn find_duplicate4(nums: Vec<i32>) -> i32 {
    let mut nums = nums;
    let len = nums.len();
    for i in 0..len {
        let index: i32 = nums[i].abs();
        let index_usize: usize = index as usize;
        if nums[index_usize] < 0 {
            return index;
        }
        nums[index_usize] *= -1;
    }
    -1
}
}

时间复杂度是 O(n), 空间复杂度是 O(1)

问题 0301-0400

0303. 区域和检索 - 数组不可变 Range Sum Query - Immutable

问题描述

这个问题就是要使用 前缀和数组 prefix sum array, 详细的内容可以参阅前文的介绍.

实现方法也很直接:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct NumArray {
    prefix_sum: Vec<i32>,
}

impl NumArray {
    pub fn new(nums: Vec<i32>) -> Self {
        assert!(!nums.is_empty());
        let mut prefix_sum = vec![0; nums.len()];
        prefix_sum[0] = nums[0];

        for i in 1..nums.len() {
            prefix_sum[i] = nums[i] + prefix_sum[i - 1];
        }
        Self { prefix_sum }
    }

    #[must_use]
    pub fn sum_range(&self, left: i32, right: i32) -> i32 {
        let left = left as usize;
        let right = right as usize;
        debug_assert!(left <= right);

        if left == 0 {
            self.prefix_sum[right]
        } else {
            self.prefix_sum[right] - self.prefix_sum[left - 1]
        }
    }
}
}

算法的时间复杂度是 O(1), 空间复杂度是 O(n).

0322. 零钱兑换 Coin Change

问题描述

0326. 3的幂 Power of Three

问题描述

可以看一下下面列出的相关问题, 这三个问题有相同的解法, 也有不同的解法.

像暴力法 (brute force), 递归法以及迭代法, 都是相通的, 我们就不再重复介绍了. 但仍然列出它们的源代码在下面.

暴力法:

#![allow(unused)]
fn main() {
// 暴力法
pub fn is_power_of_three1(n: i32) -> bool {
    if n <= 0 {
        return false;
    }
    if n == 1 {
        return true;
    }

    let mut power: i32 = 1;
    while power < n {
        let (new_power, is_overflow) = power.overflowing_mul(3);
        if is_overflow {
            return false;
        }
        power = new_power;
        if power == n {
            return true;
        }
    }
    false
}
}

递归法:

#![allow(unused)]
fn main() {
// 递归法
pub fn is_power_of_three2(n: i32) -> bool {
    if n <= 0 {
        return false;
    }
    if n == 1 {
        return true;
    }

    if n % 3 != 0 {
        return false;
    }
    is_power_of_three2(n / 3)
}
}

将递归法改写为迭代的形式:

#![allow(unused)]
fn main() {
// 将递归法改写为迭代的形式
pub fn is_power_of_three3(n: i32) -> bool {
    if n <= 0 {
        return false;
    }
    if n == 1 {
        return true;
    }

    let mut n = n;
    while n % 3 == 0 {
        n /= 3;
    }
    n == 1
}
}

接下来, 介绍一下更快捷的方法:

指数-对数法

利用公式 3 ^ log3(n) == n 来计算, 先计算整数的对数, 再计算幂指数, 如果能得到相同的整数, 那就 说明计算对数时, 是被整除的, 没有小数部分被舍去. 这也就说明了 n 就是3的次幂.

power-log

这个方法也是通用的, 可以用来计算任意正整数的次幂.

#![allow(unused)]
fn main() {
// 指数-对数法
// 利用公式 3 ^ log3(n) == n 来计算
pub fn is_power_of_three5(n: i32) -> bool {
    if n <= 0 {
        return false;
    }

    3_i32.pow(n.ilog(3)) == n
}
}

质数的次幂的特性

这个解法也挺有趣的, 它利用了质数的次幂的特性:

  • 假设, max_exp 是整数范围(i32::MIN..=i32::MAX) 内3的最大的次幂
  • 如果 n == 3^x, 而 max_exp = 3^max_x, 则 max_exp % n == 0
  • 如果 max_exp = 3^max_x, 而 max_exp = m * n, 则 m 和 n 都是3的次幂

也就是说, 我们只要找到整数范围(i32::MIN -> i32::MAX) 内3的最大的次幂, 我然后用它除以目标整数, 如果余数为0, 则3的最大次幂是这个目标整数的积; 否则, 该目标整数便不是3的次幂.

如果不好理解的话, 可以看下图: max-exp

要晓得的是, 这个方法只对质数的次幂有效.

关于如何计算最大次幂, 代码里的辅助函数有说明, 要注意的一点是整数相乘的溢出问题, 我们是利用了 i32::overflowing_mul() 方法来处理的.

#![allow(unused)]
fn main() {
// 利用质数次幂的特性:
// 如果 n == 3^x, 而 max_n = 3^max_x, 则 max_n % n == 0
pub fn is_power_of_three4(n: i32) -> bool {
    if n <= 0 {
        return false;
    }

    // 找到 i32 中 3 的最大次幂
    const fn max_exp_of_prime_number(prime_number: i32) -> i32 {
        // debug_assert!(is_prime(prime_number));
        let mut exp: i32 = 1;
        loop {
            let (next_exp, is_overflow) = exp.overflowing_mul(prime_number);
            if is_overflow {
                break;
            }
            exp = next_exp;
        }
        exp
    }

    let max_exp = max_exp_of_prime_number(3);
    max_exp % n == 0
}
}

相关问题

0338. 比特位计数 Counting Bits

问题描述

方法1, Brute Force

很容易就有了这个想法. 遍历 0..=n 的整数, 然后依次计算每个整数中比特位为1的个数. Rust 提供了 i32::count_ones() 这样的方法, 它内部是调用的本平台的汇编指令, 还是很快的.

时间复杂度是 O(n).

#![allow(unused)]
fn main() {
// Brute Force
pub fn count_bits1(n: i32) -> Vec<i32> {
    assert!(n >= 0);
    (0..=n).map(|i| i.count_ones() as i32).collect()
}
}

方法2, 使用动态规划

这个方法利用了一个重要的特性: f(n) = f(n/2) + lsb

解释一下就是, 整数 n 右移一位, 丢弃掉它的最低有效位 (least significant bit, lsb) 后, 就是 n/2

可以看下面的图:

left-shift

时间复杂度是 O(n).

#![allow(unused)]
fn main() {
// Dynamic Programming
pub fn count_bits2(n: i32) -> Vec<i32> {
    assert!(n >= 0);
    let mut vec = vec![0; n as usize + 1];
    for i in 0..=n {
        let i_usize = i as usize;
        // f(n) = f(n/2) + lsb
        // 下面两行的写法是等效的:
        //vec[i_usize] = vec[i_usize / 2] + i % 2;
        vec[i_usize] = vec[i_usize >> 1] + (i & 1);
    }
    vec
}
}

0342. 4的幂 Power of Four

问题描述

这个问题与 0231. 2 的幂 Power of Two 很相似, 可以参考下它的解法.

方法1, 暴力法

直接找到所有 4^x 的值, 看它们是否与目标整数相等.

         1 = 0b00000000000000000000000000000001
         4 = 0b00000000000000000000000000000100
        16 = 0b00000000000000000000000000010000
        64 = 0b00000000000000000000000001000000
       256 = 0b00000000000000000000000100000000
      1024 = 0b00000000000000000000010000000000
      4096 = 0b00000000000000000001000000000000
     16384 = 0b00000000000000000100000000000000
     65536 = 0b00000000000000010000000000000000
    262144 = 0b00000000000001000000000000000000
   1048576 = 0b00000000000100000000000000000000
   4194304 = 0b00000000010000000000000000000000
  16777216 = 0b00000001000000000000000000000000
  67108864 = 0b00000100000000000000000000000000
 268435456 = 0b00010000000000000000000000000000
1073741824 = 0b01000000000000000000000000000000

然后, 代码也比较直接:

#![allow(unused)]
fn main() {
// Brute Force
pub fn is_power_of_four1(n: i32) -> bool {
    if n <= 0 {
        return false;
    }
    if n == 1 {
        return true;
    }

    let mut power = 1;
    while 0 < power && power < n {
        power <<= 2;
        if power == n {
            return true;
        }
    }
    false
}
}

方法2, 递归法

这个方法是根据题目要求实现的, 也是 余数与商(DivMod) 问题.

div-mode

如果整数 n 是4的次幂, 即 n == 4^x:

  • 那么 n / 4 == 4 ^ (x - 1) 等式也是成立的, 即 n / 4 也是4的次幕
  • 而且 n % 4 == 0

基于上面的等式, 可以很容易地写出递归的代码:

#![allow(unused)]
fn main() {
// 递归法
pub fn is_power_of_four2(n: i32) -> bool {
    if n == 1 {
        return true;
    }
    if n <= 0 || n % 4 != 0 {
        return false;
    }
    is_power_of_four2(n / 4)
}
}

方法3, 重写递归法为迭代的形式

这个就比较简单了, 但要注意边界条件.

#![allow(unused)]
fn main() {
// 把递归法改写为迭代的形式
pub fn is_power_of_four3(n: i32) -> bool {
    if n == 0 {
        return false;
    }
    if n == 1 {
        return true;
    }

    let mut n = n;
    while n % 4 == 0 {
        n /= 4;
    }
    n == 1
}
}

方法4, 利用 4^x 的特性

注意看方法1中展示出的 4^x 整数的二进制形式, 是不是很有特点?

  • 只有一个比特位的值为1
  • 低位比特位为0的个数是偶数个, 这个也好理解, 因为 4^x 就相当于 x << 2, 要左移2位, 自然在低字节位会留下偶数个的0

利用以上这两个条件就可以判断整数是否为4的次幂, 代码如下:

#![allow(unused)]
fn main() {
// 整数为 4^x 的特性
pub fn is_power_of_four4(n: i32) -> bool {
    if n <= 0 {
        return false;
    }
    n.count_ones() == 1 && n.trailing_zeros() % 2 == 0
}
}

相关问题

0347. 前 K 个高频元素 Top K Frequent Elements

问题描述

优先级队列

因为要计算 top-k 的问题, 我们自然想到了优先级队列 Priority Queue.

  • 同样是先用 hashmap 来统计各整数出现的频率
  • 然后将它们存放到一个最大堆 heap 中, 每个元素是一个元组, 包含 (频率, 整数值) 两项, 以频率和整数值的降序来排列
  • 之后将最大堆转换成数组, 并截取前 k 个元素即可

Rust 实现

#![allow(unused)]
fn main() {
use std::collections::{BinaryHeap, HashMap};

// HashMap + Priority Queue
// 字典计数
pub fn top_k_frequent1(nums: Vec<i32>, k: i32) -> Vec<i32> {
    assert!(!nums.is_empty());
    assert!(k > 0);

    // 计数
    let mut map: HashMap<i32, usize> = HashMap::new();
    for &num in &nums {
        map.entry(num).and_modify(|count| *count += 1).or_insert(1);
    }

    // 优先队列, BinaryHeap 是一个最大堆
    let k = k as usize;
    let mut heap = BinaryHeap::with_capacity(map.len());
    for (num, count) in map {
        heap.push((count, num));
    }

    // 转换成数组.
    let mut out = Vec::with_capacity(k);
    while let Some(top) = heap.pop() {
        out.push(top.1);
        if out.len() == k {
            break;
        }
    }
    out
}
}

Python 实现

import heapq

class Solution:
    def topKFrequent(self, nums: list[int], k: int) -> list[int]:
        # 首先统计数值的频率
        word_count = {}
        for num in nums:
            word_count[num] = word_count.get(num, 0) + 1
        # 构造最大堆, 堆中的元素是 (频率, 数值)
        pq = [(count, value) for (value, count) in word_count.items()]
        heapq.heapify(pq)
        # 得到最大堆的 top-k
        lst = heapq.nlargest(k, pq)
        # 提取 top-k 中的数值
        return [value for (_count, value) in lst]

C++ 实现

#include <queue>
#include <unordered_map>
#include <vector>

class Solution {
 public:
  std::vector<int> topKFrequent(std::vector<int>& nums, int k) {
    // 先计算各数值出现的频率
    // (number, freq)
    std::unordered_map<int, size_t> freqs;
    for (int num: nums) {
      freqs[num] += 1;
    }

    // 再将它们存入到 priority_queue, 它是个最大堆.
    // (freq, number), 以降序排列
    std::priority_queue<std::pair<size_t, int>> queue;
    for (const auto& pair : freqs) {
      queue.emplace(pair.second, pair.first);
    }

    // 最后导出为 vector
    std::vector<int> out;
    while (!queue.empty() && out.size() < k) {
      out.emplace_back(queue.top().second);
      queue.pop();
    }

    return out;
  }
};

手动对数值频率进行排序

上面的方法使用了最大堆来对数值出现的频率进行了排序, 但我们发现它并不是最快的算法.

  • 我们可以将有相同频率的所有数值都存放在同一个数组中
  • 然后用一个大的数组来存放, 以数值的频率作为元素的索引

代码实现如下:

#![allow(unused)]
fn main() {
use std::collections::{BinaryHeap, HashMap};

// HashMap + Bucket
pub fn top_k_frequent3(nums: Vec<i32>, k: i32) -> Vec<i32> {
    assert!(!nums.is_empty());
    assert!(k > 0);

    // 计数
    let mut count_map: HashMap<i32, usize> = HashMap::new();
    for &num in &nums {
        *count_map.entry(num).or_insert(0) += 1;
    }

    // 将有相同频率的数值放在一个数组中.
    let max_count: usize = count_map.values().max().copied().unwrap_or_default();
    // 要注意数组的元素个数是 max_count + 1
    let mut count_list = vec![Vec::new(); max_count + 1];
    for (&num, &count) in &count_map {
        count_list[count].push(num);
    }

    // 从最高频率开始, 依次收集整数值.
    let k_usize = k as usize;
    let mut out = Vec::new();
    for array in count_list.into_iter().rev() {
        if !array.is_empty() {
            out.extend(&array);
        }
        if out.len() >= k_usize {
            break;
        }
    }
    out
}
}

Quick Select

TODO

Counting Sort

TODO

0349. 两个数组的交集 Intersection of Two Arrays

问题描述

这是一个搜索问题, 这个问题的解法就比较多了.

并行双指针 Parallel Two Pointers

这个也比较符合并行双指针的使用场景, 遍历两个数组或链表, 用它能很快地计算集合的交集和并集.

parallel two-pointers

在使用双指针之前, 要先对数组进行排序, 让重复的元素挨在一起.

这种方法也合适输出结果包含重复元素的问题, 只要不去重即可.

#![allow(unused)]
fn main() {
// 并行双指针
pub fn intersection1(nums1: Vec<i32>, nums2: Vec<i32>) -> Vec<i32> {
    let mut nums1 = nums1;
    let mut nums2 = nums2;
    // 先给数组排序
    nums1.sort();
    nums2.sort();

    let mut index1 = 0;
    let mut index2 = 0;
    let len1 = nums1.len();
    let len2 = nums2.len();
    let mut out = Vec::with_capacity(len1 + len2);

    // 遍历两个数组
    while index1 < len1 && index2 < len2 {
        match nums1[index1].cmp(&nums2[index2]) {
            Ordering::Less => {
                index1 += 1;
            }
            Ordering::Equal => {
                let val = nums1[index1];
                out.push(val);
                // 跳过重复的元素
                while index1 < len1 && nums1[index1] == val {
                    index1 += 1;
                }
                while index2 < len2 && nums2[index2] == val {
                    index2 += 1;
                }
            }
            Ordering::Greater => {
                index2 += 1;
            }
        }
    }
    out
}
}

HashSet 的集合操作

Rust 语言的 HashSet 实现了集合操作, 我们可以先把数组成转 HashSet, 再利用它的 HashSet::intersection() 方法, 求出交集, 最后再把交集转换回数组即可.

整个方法代码量很少, 很简洁, 但性能不是最好的.

这种方法只合适输出结果不包含重复元素的问题, 如果要包含重复元素的话, 可以将 HashSet 换成 HashMap.

#![allow(unused)]
fn main() {
// 使用 HashSet 集合操作
pub fn intersection2(nums1: Vec<i32>, nums2: Vec<i32>) -> Vec<i32> {
    let set1: HashSet<i32> = nums1.into_iter().collect();
    let set2: HashSet<i32> = nums2.into_iter().collect();
    set1.intersection(&set2).copied().collect()
}
}

使用 Bitset

当一个数组是无序的, 而且里面也有重复元素时, 我们可以把它转换 BitSet, 而我们并不关心重复元素时, 可以实现对数组元素的快速查找.

C++ 这样的语言在标准库里自带了 BitSet, 但在 Rust 标准库里却没有, 还好它的实现不算复杂.

我们使用 Vec<bool> 来简化 BitSet 的实现, 性能会差一些.

#![allow(unused)]
fn main() {
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct BitSet {
    bits: Vec<bool>,
}

impl BitSet {
    #[must_use]
    #[inline]
    pub const fn new() -> Self {
        Self { bits: Vec::new() }
    }

    #[must_use]
    #[inline]
    pub fn with_capacity(capacity: usize) -> Self {
        Self {
            bits: Vec::with_capacity(capacity),
        }
    }

    #[inline]
    pub fn set(&mut self, index: usize) {
        if index >= self.bits.len() {
            self.bits.resize(index + 1, false);
        }
        self.bits[index] = true;
    }

    #[inline]
    pub fn unset(&mut self, index: usize) {
        if index < self.bits.len() {
            self.bits[index] = false;
        }
    }

    #[must_use]
    #[inline]
    pub fn is_set(&self, index: usize) -> bool {
        if index < self.bits.len() {
            self.bits[index]
        } else {
            false
        }
    }

    #[inline]
    pub fn to_vec(&self) -> Vec<usize> {
        // TODO(shaohua): Impl Iterator and IntoIter traits
        self.bits
            .iter()
            .enumerate()
            .filter(|(_index, &is_set)| is_set)
            .map(|(index, _is_set)| index)
            .collect()
    }
}

impl FromIterator<usize> for BitSet {
    fn from_iter<T>(iter: T) -> Self
    where
        T: IntoIterator<Item = usize>,
    {
        let iterator = iter.into_iter();
        let capacity = match iterator.size_hint() {
            (_, Some(upper_size)) => upper_size,
            (size, None) => size,
        };
        let mut set = BitSet::with_capacity(capacity);
        for num in iterator {
            set.set(num)
        }
        set
    }
}
}

实现 BitSet 类之后, 就可以用它来存储 nums1 了. 要注意的点有两个:

  • 交集, 元素要在 nums1nums2 中都存在
  • 输出的结果不允许有重复的元素

bitset

这种方法只适合输出结果中不包含重复元素的问题.

#![allow(unused)]
fn main() {
// 优化上面的方法, 只使用一个 bitset
pub fn intersection4(nums1: Vec<i32>, nums2: Vec<i32>) -> Vec<i32> {
    // 将 nums1 转换为 bitset.
    let mut set1: BitSet = nums1.iter().map(|&val| val as usize).collect();
    let mut out = Vec::new();

    // 遍历 nums2
    for &num in &nums2 {
        let num_usize = num as usize;
        // num 在 set1 中也存在
        if set1.is_set(num_usize) {
            out.push(num);
            // 重置 set1 中的值, 因为它已经被插入到了结果中, 不能再重复使用.
            set1.unset(num_usize);
        }
    }
    out
}
}

上面提到了交集的两个特点:

  • 交集, 元素要在 nums1nums2 中都存在
  • 输出的结果不允许有重复的元素

除了使用 HashSet 和 BitSet 之外, 我们也可以在原地给数组排序并去除重复元素. 然后遍历 nums1, 并用二分查找法检查这个元素在 nums2 中是否同样存在.

这种方法只适合输出结果中不包含重复元素的问题.

#![allow(unused)]
fn main() {
// 二分查找法
pub fn intersection5(nums1: Vec<i32>, nums2: Vec<i32>) -> Vec<i32> {
    let mut nums1 = nums1;
    let mut nums2 = nums2;
    // 先给数组排序
    nums1.sort();
    nums2.sort();
    // 去掉重复元素
    nums1.dedup();
    nums2.dedup();

    let mut out = Vec::new();

    // 遍历 nums1, 并使用二分查找法检查该元素在 nums2 中是否也存在.
    for num in &nums1 {
        if nums2.binary_search(num).is_ok() {
            out.push(*num);
        }
    }

    out
}
}

相关问题

0350. 两个数组的交集 II Intersection of Two Arrays II

问题描述

这类问题在 0349. 两个数组的交集 Intersection of Two Arrays 中 有好几个解法.

但是如果允许存在重复元素的话, 那可用的算法就少一些了.

并行双指针

这个方法依然有效, 只需要不跳过重复元素即可.

parallel two-pointers

关于并行双指针的详细说明可以看这里.

#![allow(unused)]
fn main() {
// 并行双指针
pub fn intersect1(nums1: Vec<i32>, nums2: Vec<i32>) -> Vec<i32> {
    let mut nums1 = nums1;
    let mut nums2 = nums2;
    // 先给数组排序
    nums1.sort();
    nums2.sort();

    let mut index1 = 0;
    let mut index2 = 0;
    let len1 = nums1.len();
    let len2 = nums2.len();
    let mut out = Vec::new();

    // 同时遍历两个数组, 只要有一个遍历完成, 就中止.
    while index1 < len1 && index2 < len2 {
        match nums1[index1].cmp(&nums2[index2]) {
            // 两个值不相等, 只移动元素值比较小的那个指针, 另一个指针保持不动.
            Ordering::Less => index1 += 1,
            Ordering::Greater => index2 += 1,
            // 两个元素值相等, 属于交集里的.
            Ordering::Equal => {
                out.push(nums1[index1]);
                // 同时移动两个指针.
                index1 += 1;
                index2 += 1;
                // 这里, 并不需要忽略或者跳过重复元素, 因为它们也是有效的.
            }
        }
    }
    out
}
}

使用哈稀表来计数

0349. 两个数组的交集 Intersection of Two Arrays 有提到过, HashSet 可以用来处理不包含重复元素的集合, 而 HashMap 可以用来处理包含有重复元素的集合.

hash-table

#![allow(unused)]
fn main() {
// 用哈稀表计数
pub fn intersect2(nums1: Vec<i32>, nums2: Vec<i32>) -> Vec<i32> {
    // 用哈稀表存储有较少元素的数组.
    let (nums1, nums2) = if nums1.len() < nums2.len() {
        (nums1, nums2)
    } else {
        (nums2, nums1)
    };

    // 用哈稀表来给 nums1 中的元素计数.
    let mut map1 = HashMap::new();
    for num in &nums1 {
        map1.entry(num).and_modify(|count| *count += 1).or_insert(1);
    }

    let mut out = Vec::new();

    // 遍历 nums2.
    for num in &nums2 {
        if let Some(count) = map1.get_mut(num) {
            // 如果该元素在 map1 中存在, 就将其计数值减1.
            *count -= 1;
            out.push(*num);
            if *count == 0 {
                map1.remove(num);
            }
        }
    }
    out
}
}

相关问题

0374. 猜数字大小 Guess Number Higher or Lower

问题描述

这个问题就是二分查找法的一个基本应用:

#![allow(unused)]
fn main() {
// Binary Search
unsafe fn guess_number(n: i32) -> i32 {
    assert!(n > 0);
    let mut left = 0;
    let mut right = n;
    while left <= right {
        let mid = left + (right - left) / 2;
        let pick = unsafe { guess(mid) };
        if pick == -1 {
            right = mid.saturating_sub(1);
        } else if pick == 1 {
            left = mid + 1;
        } else {
            return mid;
        }
    }
    0
}
}

0394. 字符串解码 Decode String

问题描述

这个问题也需要用栈, 因为有先入后入的操作. 但不同之处在于要使用两个栈, 分别存储数字和字符串.

另外, 在遍历字符串时, 要分别拼装数字和字符串.

3[a]2[bc] 为例:

stack

代码实现

Rust

#![allow(unused)]
fn main() {
// Stack
pub fn decode_string1(s: String) -> String {
    assert!(!s.is_empty());

    // 用于存储 '[' 之前的数字, 可能是多位数.
    let mut num_stack: Vec<i32> = Vec::new();
    // 存储 '[' 之前的字符串.
    let mut str_stack: Vec<Vec<char>> = Vec::new();

    let chars: Vec<char> = s.chars().collect();
    // 存放当前的字符串.
    let mut parts: Vec<char> = Vec::new();
    // 存放当前的数字.
    let mut num: i32 = 0;

    for chr in chars {
        match chr {
            chr if chr.is_ascii_digit() => {
                // 处理数字
                let digit = chr.to_digit(10).unwrap() as i32;
                num = num * 10 + digit;
            }
            '[' => {
                // 将 '[' 之前的数字和字符串入栈.
                num_stack.push(num);
                str_stack.push(parts.clone());

                // 并重置它们.
                parts.clear();
                num = 0;
            }
            ']' => {
                // 收网, 从两个栈中分别取出整数和字符串, 进行一次拼装,
                // 然后将拼装结果入字符串栈.
                //
                // curr_num 是当前字符串重复次数.
                let curr_num = num_stack.pop().unwrap();
                // last_parts 是 '[' 之前的字符串, 相当于当前字符串的前缀.
                let last_parts = str_stack.pop().unwrap();
                // 合成的新的字符串.
                // parts = last_parts + curr_parts * curr_num.
                let curr_parts = parts;
                parts = last_parts;

                for _i in 0..curr_num {
                    parts.extend_from_slice(&curr_parts);
                }
            }
            letter => {
                // 拾取所有字符
                parts.push(letter);
            }
        }
    }

    // 组装最后的字符串
    parts.into_iter().collect()
}
}

C++

#include <cassert>

#include <stack>
#include <string>

std::string decode_string(std::string s) {
  // 数字栈, 用于存放遇到的数字.
  std::stack<int> num_stack;
  // 字符串栈, 用于存放中间的字符串.
  std::stack<std::string> str_stack;

  // 存放当前的数字
  int num = 0;
  // 存放当前的字符串
  std::string letters;

  for (char chr : s) {
    if (std::isdigit(chr)) {
      // 数字
      const int digit = static_cast<int>(chr - '0');
      num = num * 10 + digit;
    } else if (chr == '[') {
      // 将当前的数字和字符串入栈
      num_stack.push(num);
      num = 0;

      str_stack.push(letters);
      letters.clear();
    } else if (chr == ']') {
      // 遇到了右括号, 进行字符串的拼装.
      int last_num = num_stack.top();
      num_stack.pop();

      // new_letters = last_str + last_num * letters
      std::string last_str = str_stack.top();
      str_stack.pop();

      std::string new_letters = last_str;
      for (int i = 0; i < last_num; ++i) {
        new_letters += letters;
      }
      letters = new_letters;
    } else {
      // 其它字符
      letters += chr;
    }
  }

  return letters;
}

问题 0401-0500

0463. 岛屿的周长 Island Perimeter

问题描述

0468. 验证IP地址 Validate IP Address

问题描述

这个是处理字符串的问题.

步骤大致如下:

  • 先检查是不是 ipv4, 如果是, 直接返回
    • 先以 . 将字符串分隔成多个部分, parts
    • parts 数组的长度应该是 4
    • 检查每一部分字符串
      • 长度在 [1..3] 之间
      • 如果以 0 为前缀的话, 只能包含 0
      • 检查里面的字符, 只能包含 0-9 这10 个字符, 可以用 std::isdigit(c)
      • 将它转换成整数, 数值范围是 [0..255]
  • 再检查是不是 ipv6, 如果是, 就返回
    • : 将字符串分隔成多个部分, parts
    • parts 数组的长度是 8
    • 检查每一部分字符串
      • 字符串长度是 [1..4] 之间
      • 检查里面的字符, 只能包含 0-9, a-f, A-F这些字符, 可以用 std::is_xdigit(c)
      • 不需要把它再转换成整数
  • 返回 Neither

以下是代码实现:

Rust

#![allow(unused)]
fn main() {
fn is_ipv4(query: &str) -> bool {
    // 用 `.` 来分隔各部分
    // 并判断每个部分是有效的数值
    // 数值不带有前缀0

    let parts: Vec<&str> = query.split('.').collect();
    if parts.len() != 4 {
        return false;
    }

    for part in parts {
        if part.is_empty() || part.len() > 3 {
            return false;
        }

        // 数值不带有前缀0
        if part.len() > 1 && part.starts_with("0") {
            return false;
        }

        // 判断字符的范围, 0-9
        for c in part.chars() {
            if !c.is_ascii_digit() {
                return false;
            }
        }

        if let Ok(val) = part.parse::<i32>() {
            // 数值范围是 0..255
            if !(0..=255).contains(&val) {
                return false;
            }
        } else {
            // 不是有效的整数
            return false;
        }
    }

    true
}

fn is_ipv6(query: &str) -> bool {
    // 使用 `:` 作为分隔符
    // 每个部分是16进制的整数, 16进制支持大小写, 最多包含4个字符
    // 可以有0作为前缀
    // 不需要考虑缩写

    let parts: Vec<&str> = query.split(':').collect();
    if parts.len() != 8 {
        return false;
    }

    for part in parts {
        // 1-4个字符
        if part.is_empty() || part.len() > 4 {
            return false;
        }

        for c in part.chars() {
            // 判断字符的范围, 0-9, a-f, A-F
            if !c.is_ascii_hexdigit() {
                return false;
            }
        }
    }

    true
}

pub fn valid_ip_address1(query_ip: String) -> String {
    if is_ipv4(&query_ip) {
        "IPv4".to_owned()
    } else if is_ipv6(&query_ip) {
        "IPv6".to_owned()
    } else {
        "Neither".to_owned()
    }
}
}

C++

#include <cassert>

#include <iostream>
#include <sstream>
#include <string>
#include <sstream>

class Solution {
 public:
  static bool isIPv4(const std::string& query) {
    // 用 `.` 来分隔各部分
    // 并判断每个部分是有效的数值
    // 数值不带有前缀0

    if (query.empty() || query[0] == '.' || query[query.size() - 1] == '.') {
      return false;
    }
    int part_count = 0;
    std::stringstream ss(query);
    std::string part;
    while (std::getline(ss, part, '.')) {
      // 数值不带有前缀0
      if (part[0] == '0' && part.size() > 1) {
        return false;
      }
      if (part.size() < 1 || part.size() > 3) {
        return false;
      }
      // 判断字符的范围, 0-9
      for (char c : part) {
        if (!std::isdigit(c)) {
          return false;
        }
      }

      size_t pos = 0;
      const int val = std::stoi(part, &pos);
      // 不是有效的整数
      if (pos != part.size()) {
        //return false;
      }

      // 数值范围是 0..255
      if (val < 0 || val > 255) {
        return false;
      }

      part_count += 1;
    }

    // 要有4个部分
    return part_count == 4;
  }

  static bool isIPv6(const std::string& query) {
    // 使用 `:` 作为分隔符
    // 每个部分是16进制的整数, 16进制支持大小写, 最多包含4个字符
    // 可以有0作为前缀
    // 不需要考虑缩写

    if (query.empty() || query[0] == ':' || query[query.size() - 1] == ':') {
      return false;
    }

    int part_count = 0;
    std::stringstream ss(query);
    std::string part;
    while (std::getline(ss, part, ':')) {
      // 1-4个字符
      if (part.size() < 1 || part.size() > 4) {
        return false;
      }

      for (char c : part) {
        // 判断字符的范围, 0-9, a-f, A-F
        if (!std::isxdigit(c)) {
          return false;
        }
      }

      part_count += 1;
    }

    return part_count == 8;
  }

  static std::string validIPAddress(std::string queryIP) {
    if (isIPv4(queryIP)) {
      return "IPv4";
    } 
    if (isIPv6(queryIP)) {
      return "IPv6";
    }
    return "Neither";
  }
};

0485. 最大连续1的个数 Max Consecutive Ones

问题描述

滑动窗口

这是一个简单的滑动窗口的问题.

遍历数组, 计算出每个窗口的大小, 然后找出最大值即可.

sliding-window

要注意的点是:

  • 遍历完数组后, 要检查最后一个窗口是不是最大值
#![allow(unused)]
fn main() {
// Sliding window
pub fn find_max_consecutive_ones1(nums: Vec<i32>) -> i32 {
    let mut max_count = 0;
    let mut count = 0;
    for &num in &nums {
        if num != 1 {
            max_count = max_count.max(count);
            count = 0;
        } else {
            count += 1;
        }
    }
    max_count.max(count)
}
}

0496. 下一个更大元素 I Next Greater Element I

问题描述

暴力法

思路比较简单:

  • 遍历 nums1 数组中的所有元素, 当前元素为 x
  • 遍历 nums2 数组中的所有元素, 找到第一个与 x 相等的元素, 然后向右继续遍历, 找到第一个比 x 大的元素

这个算法的时间复杂度是 O(n^2).

算法实现如下:

#![allow(unused)]
fn main() {
/// Brute force
pub fn next_greater_element1(nums1: Vec<i32>, nums2: Vec<i32>) -> Vec<i32> {
    // 用于记录每次遍历时的状态.
    #[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
    enum State {
        /// 没有找到相等的值
        #[default]
        NotFound,
        /// 找到了相等的值
        FoundEqual,
        /// 找到了相等的值, 同时在它的右侧也找到了更大的值
        FoundGreater,
    }

    let mut out = Vec::with_capacity(nums1.len());
    for x in nums1 {
        let mut state = State::NotFound;
        for &y in &nums2 {
            if state == State::NotFound && y == x {
                state = State::FoundEqual;
            }
            if state == State::FoundEqual && y > x {
                out.push(y);
                state = State::FoundGreater;
            }
        }
        if state != State::FoundGreater {
            out.push(-1);
        }
    }
    out
}
}

单调栈

因为要在 nums2 数组中找到当前元素, 以及当前元素右侧第一个更大的元素, 这个比较符合单调栈的操作.

具体操作是:

  • 先遍历数组 nums2, 构造单调递增栈; 同时创建一个哈稀表, 用于存储 (当前元素值, 当前元素右侧第一个大的元素值) 映射关系 map
    • 如果当前元素比栈顶元素小, 直接入栈
    • 否则说明当前元素比栈顶元素大, 依次将栈顶元素出栈, 并存入 map 中. 这个栈顶元素的右侧第一个大的元素值就是当前元素
  • 之后遍历 nums1 数组, 从 map 哈稀表中找到对应的更大的元素, 如果没有的话, 就存储为 -1

算法实现如下:

#![allow(unused)]
fn main() {
use std::collections::HashMap;

/// 使用单调栈
pub fn next_greater_element2(nums1: Vec<i32>, nums2: Vec<i32>) -> Vec<i32> {
    let mut monotonic_stack = Vec::with_capacity(nums2.len());
    assert!(!nums2.is_empty());
    let mut max_num = i32::MAX;
    let mut map = HashMap::new();

    // 构造递增式单调栈
    for &num in &nums2 {
        if num < max_num {
            // 将较小的元素入栈
            max_num = num;
            monotonic_stack.push(num);
        } else {
            // 将较小的元素出栈
            while !monotonic_stack.is_empty() && monotonic_stack[monotonic_stack.len() - 1] < num {
                let top = monotonic_stack.pop().unwrap();
                map.insert(top, num);
            }
            // 将当前元素入栈
            monotonic_stack.push(num);
        }
    }

    let out = nums1
        .iter()
        .map(|num1| map.get(num1).copied().unwrap_or(-1))
        .collect();

    out
}
}

C++ 实现如下:

#include <cassert>
#include <climits>

#include <iostream>
#include <stack>
#include <string>
#include <unordered_map>
#include <vector>

std::vector<int> nextGreaterElement(std::vector<int>& nums1, std::vector<int>& nums2) {
  std::stack<int> monotonic_stack;
  std::unordered_map<int, int> map;
  int max_num = INT_MAX;

  // 构造递增式单调栈
  for (int num2 : nums2) {
    if (num2 < max_num) {
      // 将较小的元素入栈
      max_num = num2;
      monotonic_stack.push(num2);
    } else {
      // 将较小的元素出栈
      while (!monotonic_stack.empty() && monotonic_stack.top() < num2) {
        const int top = monotonic_stack.top();
        monotonic_stack.pop();
        map.emplace(top, num2);
      }
      // 将当前元素入栈
      monotonic_stack.push(num2);
    }
  }

  std::vector<int> out;
  for (int num1 : nums1) {
    auto iter = map.find(num1);
    if (iter == map.cend()) {
      out.push_back(-1);
    } else {
      out.push_back(iter->second);
    }
  }

  return out;
}

void checkSolution() {
  {
    std::vector<int> nums1 = {4, 1, 2};
    std::vector<int> nums2 = {1, 3, 4, 2};
    std::vector<int> expected = {-1, 3, -1};
    assert(nextGreaterElement(nums1, nums2) == expected);
  }

  {
    std::vector<int> nums1 = {2, 4};
    std::vector<int> nums2 = {1, 2, 3, 4};
    std::vector<int> expected = {3, -1};
    assert(nextGreaterElement(nums1, nums2) == expected);
  }

  {
    std::vector<int> nums1 = {1, 3, 5, 2, 4};
    std::vector<int> nums2 = {6, 5, 4, 3, 2, 1, 7};
    std::vector<int> expected = {7, 7, 7, 7, 7};
    assert(nextGreaterElement(nums1, nums2) == expected);
  }
}

int main() {
  checkSolution();

  return 0;
}

问题 0501-0600

0532.数组中的数对 K-diff Pairs in an Array

问题描述

这是一个查找问题, 先思考一下处理查找问题的常用手段:

  • 哈稀表或者 HashSet
  • BitSet
  • 排序后二分查找
  • 排序后快慢型双指针遍历

哈稀表 Hash Table

使用哈稀表 HashMap 来统计整数值及其次数; 用集合 HashSet 来存放的有序数对, 并去掉重复的.

hash-table

这种方法可以支持无序数组.

#![allow(unused)]
fn main() {
// 哈稀表来计数
pub fn find_pairs1(nums: Vec<i32>, k: i32) -> i32 {
    assert!(!nums.is_empty());

    let mut map = HashMap::new();
    for num in &nums {
        map.entry(num).and_modify(|count| *count += 1).or_insert(1);
    }

    // 使用集合来去重.
    let mut set = HashSet::new();
    for &num in &nums {
        // k = diff - num;
        // diff1 >= num
        let diff1 = num + k;
        if let Some(count) = map.get(&diff1) {
            if (diff1 > num) || ((diff1 == num) && (*count > 1)) {
                set.insert(vec![num, diff1]);
            }
        }

        // k = num - diff;
        // diff2 <= num
        let diff2 = num - k;
        if let Some(count) = map.get(&diff2) {
            if diff2 < num {
                set.insert(vec![diff2, num]);
            } else if (diff2 == num) && (*count > 1) {
                set.insert(vec![num, diff2]);
            }
        }
    }

    set.len() as i32
}
}

二分查找法 Binary Search

基本的思路是:

  • 先给数组排序
  • 开始遍历数组
  • 根据题目条件, 确定目标的元素的值; 使用二分查找法搜索目标元素
  • 再根据要求, 判断目标元素是否合适, 比如两者索引值不能相同
#![allow(unused)]
fn main() {
// 排序后二分查找, 不使用额外内存. 根据 k == 0 做优化
pub fn find_pairs5(nums: Vec<i32>, k: i32) -> i32 {
    assert!(!nums.is_empty());

    // 先排序
    let mut nums = nums;
    nums.sort();
    let mut count = 0;
    let mut fast = 0;

    if k == 0 {
        let len = nums.len();

        // 遍历数组
        while fast < len {
            let curr_val = nums[fast];
            // 只保留两个重复元素, 跳过其它的.
            if fast + 1 < len && curr_val == nums[fast + 1] {
                count += 1;
                fast += 1;
            }
            while fast + 1 < len && curr_val == nums[fast + 1] {
                fast += 1;
            }

            // 指针向前走一步
            fast += 1;
        }
    } else {
        // 去掉重复元素
        nums.dedup();
        let len = nums.len();

        // 遍历数组
        while fast < len {
            let curr_val = nums[fast];
            let expected_val = curr_val + k;
            // 使用二分查找法在后面的元素里搜索 `expected_val`.
            if fast + 1 < len && nums[fast + 1..].binary_search(&expected_val).is_ok() {
                count += 1;
            }

            // 指针向前走一步
            fast += 1;
        }
    }

    count
}
}

快慢型双指针 Fast-Slow Two Pointers

这个方法的效率是最高的, 也最节省内存.

解决问题之前依然要先给数组排序.

fast-slow two-pointers

这个题目中, 双指针的命中条件是 nums[fast] - nums[slow] = k;, 只需要围绕这个核心条件做判断即可.

#![allow(unused)]
fn main() {
// 快慢型双指针
pub fn find_pairs6(nums: Vec<i32>, k: i32) -> i32 {
    let len = nums.len();
    if len <= 1 {
        return 0;
    }

    let mut nums = nums;
    // 先排序
    nums.sort();

    // 初始化两个指针, 两个指针不能重复.
    let mut fast = 1;
    let mut slow = 0;
    let mut count = 0;

    // 遍历整个数组.
    while fast < len {
        // 两个指针不能重复, 因为题目要求: `i != j`.
        if fast == slow {
            fast += 1;
            continue;
        }

        match (nums[fast] - nums[slow]).cmp(&k) {
            Ordering::Equal => {
                count += 1;
                let curr_slow = nums[slow];
                let curr_fast = nums[fast];

                // 跳过重复元素
                while slow < len && curr_slow == nums[slow] {
                    slow += 1;
                }
                while fast < len && curr_fast == nums[fast] {
                    fast += 1;
                }
            }
            Ordering::Less => {
                // 两个元素间的差值太小了, 移动 fast 指针
                fast += 1;
            }
            Ordering::Greater => {
                // 两个元素间的差值太大了, 移动 slow 指针
                slow += 1;
            }
        }
    }

    count
}
}

相关问题

问题 0601-0700

0679. 24 点游戏 24 Game

问题描述

TODO(Shaohua):

0680. 验证回文串 II Valid Palindrome II

问题描述

在Rust中处理字符串, 远不如处理 Vec<u8> 或者 slice 简单, 所以这里我们在有必要时, 先把字符串 转换成数组: let bytes = s.as_bytes();

暴力法

利用题目中的要求, 按序依次跳过数组中的一个元素, 并判断它组成的新数组是不是回文.

remove-one-element

#![allow(unused)]
fn main() {
// 优化暴力法
// 计算超时, 无效
pub fn valid_palindrome2(s: String) -> bool {
    fn is_palindrome_slice(s: &[u8]) -> bool {
        let mut left = 0;
        let mut right = s.len() - 1;
        while left < right {
            if s[left] != s[right] {
                return false;
            }
            left += 1;
            right -= 1;
        }
        true
    }

    let bytes = s.as_bytes();
    // 检查不跳过某一元素时的情况
    if is_palindrome_slice(bytes) {
        return true;
    }

    let mut new_bytes = bytes.to_vec();

    for i in 0..bytes.len() {
        // 跳过某一元素, 构造新的数组
        new_bytes.clear();
        new_bytes.extend_from_slice(&bytes[..i]);
        new_bytes.extend_from_slice(&bytes[i + 1..]);
        // 检查新构造出的数组
        if is_palindrome_slice(&new_bytes) {
            return true;
        }
    }
    false
}
}

这里的中间步骤, 每次都要构造一个新的数组. 而且新数组的大小与原来的数组基本是一样的. 对于有很多元素的数组来说, 遍历数组时, 每次的计算量并没有变化. 新数组的大小并没有快速收敛.

比如, 计算 s = "abcadecba", 可以看到每次循环它的计算量是不变的.

abcadecba
abcadecba
abcadecba
abcadecba
abcadecba
abcadecba
abcadecba
abcadecba
abcadecba

使用一个靠拢型双指针

这个是对暴力法的优化, 它完全不需要分配堆内存, 但思路是没有变的: 按序依次跳过数组中的一个元素, 并判断它组成的新数组是不是回文.

#![allow(unused)]
fn main() {
// 双指针法, 不需要分配新的堆内存
// 计算超时, 无效
pub fn valid_palindrome3(s: String) -> bool {
    let bytes = s.as_bytes();

    // 遍历数组, 用于跳过当前的一个元素,
    // 如果 i == bytes.len(), 则不跳过任何元素.
    for i in 0..=bytes.len() {
        // 使用靠拢型双指针来检查这个 slice 是不是回文.
        let mut left = 0;
        let mut right = bytes.len() - 1;

        let mut is_palindrome = true;
        while left < right {
            if left == i {
                left += 1;
                continue;
            }
            if right == i {
                right -= 1;
                continue;
            }

            if bytes[left] != bytes[right] {
                is_palindrome = false;
                break;
            }
            // 同时移动两个指针往中间靠拢.
            left += 1;
            right -= 1;
        }

        if is_palindrome {
            return true;
        }
    }

    false
}
}

这个优化基本无效的, 因为上面提到的核心问题没解决.

使用两个靠拢型双指针

这个是对上面方法的优化, 在外层遍历数组时, 也换成双指针法, 用于快速减少子数组中的元素个数. 这样可以 快速收敛, 这对于有大量元素的数组来说很有效.

#![allow(unused)]
fn main() {
// 使用两个靠拢型双指针, 不需要分配新的堆内存
pub fn valid_palindrome4(s: String) -> bool {
    fn is_palindrome(slice: &[u8], mut left: usize, mut right: usize) -> bool {
        while left < right {
            if slice[left] != slice[right] {
                return false;
            }
            left += 1;
            right -= 1;
        }
        true
    }

    let bytes = s.as_bytes();

    // 外层双指针, 用于遍历数组
    // 这里每次遍历, 就会减少子数组的长度, 这对于巨大的数组来说很关键.
    let mut left = 0;
    let mut right = bytes.len() - 1;
    while left < right {
        if bytes[left] != bytes[right] {
            return is_palindrome(bytes, left, right - 1) || is_palindrome(bytes, left + 1, right);
        }
        left += 1;
        right -= 1;
    }
    true
}
}

计算 s = "abcadecba", 可以看到, 其过程在快速收敛:

abcadecb
bcadec
cade
ad

相关问题

问题 0701-0800

二分查找 Binary Search

问题描述

这个题目是最基础的二分查找法.

#![allow(unused)]
fn main() {
use std::cmp::Ordering;

// Binary Search
// 直接法, 找到元素后就直接返回.
pub fn search1(nums: Vec<i32>, target: i32) -> i32 {
    // 先处理极端情况.
    if nums.is_empty() || nums[0] > target || nums[nums.len() - 1] < target {
        return -1;
    }

    // 左闭右闭区间
    let mut low = 0;
    let mut high = nums.len() - 1;

    // 退出循环的条件是 left > right.
    while low <= high {
        // 防止整数平均值溢出.
        let middle = low + (high - low) / 2;
        match nums[middle].cmp(&target) {
            Ordering::Less => low = middle + 1,
            Ordering::Equal => return middle as i32,
            Ordering::Greater => {
                if middle < 1 {
                    return -1;
                } else {
                    high = middle - 1;
                }
            }
        }
    }
    -1
}
}

二分查找法之排除法

这种是二分查找法的变体, 与上面的方法不同在于边界值的范围, 这里使用的是 左闭右开区间.

#![allow(unused)]
fn main() {
// Binary Search
// 排除法
pub fn search2(nums: Vec<i32>, target: i32) -> i32 {
    if nums.is_empty() || nums[0] > target || nums[nums.len() - 1] < target {
        return -1;
    }

    // 左闭右开区间
    let mut left = 0;
    let mut right = nums.len();

    // 终止循环的条件是 left == right
    while left < right {
        // 中间节点的计算不一样.
        let mid = left + (right - left) / 2;
        // 排除 [left, mid] 区间, 在 [mid + 1, right) 区间内查找
        if nums[mid] < target {
            left = mid + 1;
        } else {
            // 排除 [mid, right] 区间, 在 [left, mid) 区间内查找
            right = mid;
        }
    }

    // 检查剩余空间内的元素, nums[left] == nums[right] == target
    if left < nums.len() && nums[left] == target {
        left as i32
    } else {
        -1
    }
}
}

标准库中自带的二分查找法实现

标准库的 slice::binary_search() 及其变体函数, 就实现了经典的二分查找法, 性能比较好:

#![allow(unused)]
fn main() {
// Binary Search
// 使用标准库中自带的方法
pub fn search3(nums: Vec<i32>, target: i32) -> i32 {
    match nums.binary_search(&target) {
        Ok(index) => index as i32,
        Err(_) => -1,
    }
}
}

0707. 设计链表 Design Linked List

问题描述

TODO(Shaohua):

寻找数组的中心下标 Find Pivot Index

问题描述

这个问题的本质是要理解 pivot index 的数学含义:

pivot index

其关系如上图所示, 从中我们可以发现 pivot index 存在时要满足这些条件:

  • pivot 元素将数组分成左右两部分, 分别叫 prefix arraysuffix array
  • sum(prefix array) = sum(suffix array)

然而, 还有一个隐藏的条件是:

sum(prefix array) + pivot + sum(suffix array) = sum(array)

Brute force

上面的公式还可以转换成:

sum(prefix array) + pivot + sum(prefix array) = sum(array)

从左到右遍历数组中的所有元素, 循环终止的条件就是找到:

sum(prefix array) + pivot + sum(prefix array) = sum(array)

以下是代码实现:

#![allow(unused)]
fn main() {
// Brute force
pub fn pivot_index1(nums: Vec<i32>) -> i32 {
    // 第一步: 计算数组中所有元素的和
    let sum: i32 = nums.iter().sum();

    // 第二步: 从左侧遍历数组, 并计算数组的前缀和 prefix sum
    // 如果前缀和 * 2 + 当前的元组, 其和等于所有元素之和,
    // 那么当前元素所在位置就是 pivot index.
    let mut prefix_sum: i32 = 0;
    for (index, num) in nums.iter().enumerate() {
        if prefix_sum * 2 + num == sum {
            return index as i32;
        }
        prefix_sum += num;
    }
    -1
}
}

这种算法的特点是:

  • 时间复杂度是 O(n)
  • 空间复杂度是 O(1)

前缀和 Prefix Sum

直接计算 prefix sum 和 suffix sum.

这个方法跟上面的类似, 但是更好理解一些, 它不需要最开始说的公式转换:

  • 首先计算所有元素的和, 作为 suffix sum; 同时将 prefix sum 初始化为 0
  • 然后从左到右遍历数组, 将该本元从 suffix sum 中减去
    • 如果此时 prefix sum == suffix sum, 则当前元素就是 pivot, 当前位置就是 pivot index, 直接返回
    • 否则将该元素加到 prefix sum 中

算法实现如下所示:

#![allow(unused)]
fn main() {
// Prefix Sum
// 直接计算 prefix sum 和 suffix sum
pub fn pivot_index2(nums: Vec<i32>) -> i32 {
    // 第一步: 计算数组中所有元素的和, 并作为 suffix sum
    let mut suffix_sum: i32 = nums.iter().sum();

    // 第二步: 从左侧遍历数组, 并调整 prefix_sum 和 suffix_sum 的值
    // 如果它们相等了, 就终止循环
    let mut prefix_sum: i32 = 0;
    for (index, num) in nums.iter().enumerate() {
        suffix_sum -= num;
        if prefix_sum == suffix_sum {
            return index as i32;
        }
        prefix_sum += num;
    }
    -1
}
}

这种算法的特点是:

  • 时间复杂度是 O(n)
  • 空间复杂度是 O(1)

0739. 每日温度 Daily Temperatures

问题描述

0744. 寻找比目标字母大的最小字母 Find Smallest Letter Greater Than Target

问题描述

这个可以用二分查找法, 它要找到比目标元素 target 大的第一个元素.

二分查找法

思路是排除法, 因为左侧的元素较小, 优先排除左侧部分. 步骤如下:

  1. 创建两个指针, 分别指向数组的左右两侧
  2. 开始二分查找, 计算中间节点 middle 的值, 并判断:
    1. 如果 letters[middle] >= target, 说明 [left..middle] 区间的字符较小, 将 left 指针向右移, 令 left = middle + 1
    2. 否则, 说明 [middle..right] 的字符都是比 target 大的, 将 right 指针左移, 令 right = middle
  3. left == right 时, 终止循环, 确认一下 left 位置的字符是不是目标
#![allow(unused)]
fn main() {
// Binary Search
pub fn next_greatest_letter1(letters: Vec<char>, target: char) -> char {
    debug_assert!(letters.len() >= 2);

    let mut left = 0;
    let mut right = letters.len() - 1;
    while left < right {
        let middle = left + (right - left) / 2;
        if letters[middle] <= target {
            // 如果 middle 处的字符小于或者等于 target, 就将 left 向右移
            left = middle + 1;
        } else {
            // 如果 middle 处的字符大于 target, 就将 right 向左移
            right = middle;
        }
    }

    // 要判断有没有找到
    if letters[left] > target {
        letters[left]
    } else {
        letters[0]
    }
}
}

时间复杂度是 O(log(n)).

问题 0801-0900

0852. 山脉数组的峰顶索引 Peak Index in a Mountain Array

问题描述

这个问题比 0162 的条件简单一些, 它只有一个峰值存在, 但解法也差不多.

暴力法

遍历整个数组, 找到唯一的那个峰值, 也就是最大值.

#![allow(unused)]
fn main() {
// Brute Force
pub fn peak_index_in_mountain_array1(arr: Vec<i32>) -> i32 {
    debug_assert!(arr.len() >= 3);

    for i in 1..(arr.len() - 1) {
        if arr[i] > arr[i - 1] && arr[i] > arr[i + 1] {
            return i as i32;
        }
    }
    -1
}
}

时间复杂度是 O(n).

二分查找法

使用二分查找法找出峰值, 它与 middle 的元素有三种关系:

  • middle 处就是峰值
  • 峰值位于 middle 的左侧
  • 峰值位于 middle 的右侧

peak position

但我们对它进行了一下简化, 步骤是:

  1. 创建两个指针, 其中 left 指向数组的最左侧, right 指向数组最右侧
  2. 开始二分查找法的循环, 计算中间节点 middle, 并确定 middle 与峰值的关系:
    1. 如果 arr[middle] < arr[middle + 1], 说明峰值位于 middle 的右侧, 此时向右移动 left 指针, 令left = middle + 1
    2. 否则说明峰值位于 middle 处或者在其左侧, 此时向左移右 right 指针, 令 right = middle
  3. left == right 时, 找到了峰值, 终断循环并返回

算法的实现如下:

#![allow(unused)]
fn main() {
// Binary Search
// 对上面的细节作了修改
pub fn peak_index_in_mountain_array3(arr: Vec<i32>) -> i32 {
    debug_assert!(arr.len() >= 3);
    let mut left = 0;
    let mut right = arr.len() - 1;

    // 简化二分查找的条件, 最终 left 位置就是峰值.
    while left < right {
        let middle = left + (right - left) / 2;
        if arr[middle] < arr[middle + 1] {
            // 1. 峰值位于 middle 右侧
            left = middle + 1;
        } else {
            // 2. 峰值位于 middle 处或在其左侧
            right = middle;
        }
    }
    left as i32
}
}

时间复杂度是 O(log(n)).

问题 0901-1000

0925. 长按键入 Long Pressed Name

问题描述

并行双指针

典型的并行双指针问题.

typed 数组与 name 数组不一致的情况有三种:

  • 长按了, 通过 name[index1 - 1] == typed[index2] 来确认
  • 按错了, name[index1] != typed[index2]
  • 少按了, 在最后, index2 != len2

parallel two-pointer

要注意的是:

  • 如果已经遍历完 name 数组, 要检查 typed 数组有没有遍历完
  • 返回 true 的条件就是两个数组都满足条件的情况下遍历完
#![allow(unused)]
fn main() {
// 并行双指针
pub fn is_long_pressed_name1(name: String, typed: String) -> bool {
    assert!(!name.is_empty() && !typed.is_empty());
    let name = name.as_bytes();
    let typed = typed.as_bytes();
    let len1 = name.len();
    let len2 = typed.len();

    // 初始化两个指针
    let mut index1 = 0;
    let mut index2 = 0;

    // 使用双指针遍历两个数组.
    while index1 < len1 && index2 < len2 {
        if name[index1] == typed[index2] {
            // 相等时, 同时向前移动两个指针.
            index1 += 1;
            index2 += 1;
        } else if index1 > 0 && name[index1 - 1] == typed[index2] {
            // 长按了一下
            index2 += 1;
        } else {
            // 按错了
            return false;
        }
    }

    // 如果已经遍历完了 name 数组, 还没遍历完 typed 数组,
    // 接下来遍历 typed 里剩下的元素.
    // 遍历的唯一条件就是 typed 里面剩下的元素都和 name 数组最后一个元素相同,
    // 否则就是按错了.
    while index2 < len2 && name[len1 - 1] == typed[index2] {
        index2 += 1;
    }

    // 检查两个数组是否都遍历完, 如果没有遍历完:
    // - index1 < len1, 少按了
    // - index2 < len2, 按错了
    index1 == len1 && index2 == len2
}
}

0977. 有序数组的平方 Squares of a Sorted Array

问题描述

TODO(Shaohua):

问题 1001-1100

1004. 最大连续1的个数 III Max Consecutive Ones III

问题描述

滑动窗口

这个问题跟之前的胡杨林补种一样, 用滑动窗口来解决:

  • 窗口右侧经过0时, 计数加1
  • 当窗口区间内的0的个数大于k 时, 把窗口左侧向右移, 直到窗口范围内的0的个数不大于k
  • 然后更新最大的连续为1的个数
#![allow(unused)]
fn main() {
// 滑动窗口
pub fn longest_ones1(nums: Vec<i32>, k: i32) -> i32 {
    // 窗口右侧经过的0 的个数, 减去窗口左侧经过的0的个数, 就是需要翻转为1的个数

    let mut left = 0;
    let mut right = 0;
    let mut num_zero = 0;
    let k = k as usize;
    let mut longest_ones = 0;

    while right < nums.len() {
        // 需要翻转
        if nums[right] == 0 {
            num_zero += 1;
        }
        // 保证最大翻转次数不大于 k
        while num_zero > k {
            // 窗口左侧右移
            if nums[left] == 0 {
                num_zero -= 1;
            }
            left += 1;
        }

        // 注意边界情况.
        longest_ones = longest_ones.max(right - left + 1);

        // 窗口右侧向右移
        right += 1;
    }

    longest_ones as i32
}
}

问题 1101-1200

问题 1201-1300

问题 1301-1400

问题 1401-1500

1422. 分割字符串的最大得分 Maximum Score After Splitting a String

问题描述

暴力法

这个用于验证思路, 直接遍历数组, 计算每个分隔位点的数值, 求得其中最大的那个.

#![allow(unused)]
fn main() {
// Brute force
pub fn max_score1(s: String) -> i32 {
    debug_assert!(s.len() >= 2);

    let mut max_value = 0;
    for i in 1..s.len() {
        let count_zeros = s[0..i]
            .as_bytes()
            .iter()
            .copied()
            .filter(|byte| *byte == b'0')
            .count();
        let count_ones = s[i..]
            .as_bytes()
            .iter()
            .copied()
            .filter(|byte| *byte == b'1')
            .count();
        let sum = count_zeros + count_ones;
        max_value = max_value.max(sum);
    }
    max_value as i32
}
}

这个算法的时间复杂度是 O(n^2), 因为存在两层遍历数组的操作; 空间复杂度是 O(1).

前缀和 Prefix sum

这个方法, 先计算好每个分隔位点之前的 0 的个数以及分隔位点之后的 1 的个数, 然后计算其中最大的那个组合.

具体步骤是:

  1. 创建 count_zeros 数组, 用于存放从前向后字符 0 出现的次数之和
  2. 从前向后遍历字符串, 统计出字符 0 出现的次数之和, 并存入 count_zeros 数组
  3. 创建 count_ones 数组, 用于存放从后向前字符 1 出现的次数之和
  4. 从后向前遍历字符串, 统计出字符 1 出现的次数之和, 并存入 count_ones 数组
  5. count_ones 数组反转, 方便后面的计算
  6. 遍历计数数组 (cont_ones, count_zeros), 找出最大的那个组合
#![allow(unused)]
fn main() {
// Prefix sum
pub fn max_score2(s: String) -> i32 {
    debug_assert!(!s.is_empty());

    let len = s.len();

    // 从前向后统计字符 `0` 出现的次数
    let count_zeros = {
        let mut count_zeros = Vec::with_capacity(len);
        let mut last_zeros = 0;
        for &byte in s.as_bytes() {
            if byte == b'0' {
                last_zeros += 1;
            }
            count_zeros.push(last_zeros);
        }
        count_zeros
    };

    // 从后向前统计字符 `1` 出现的次数
    let count_ones = {
        let mut count_ones = Vec::with_capacity(len);
        let mut last_ones = 0;
        for &byte in s.as_bytes().iter().rev() {
            if byte == b'1' {
                last_ones += 1;
            }
            count_ones.push(last_ones);
        }
        // 将 `1` 的计数数组反转
        count_ones.reverse();
        count_ones
    };

    // 遍历计数数组, 找到最大的那个组合
    let mut max_sum = 0;
    for i in 1..len {
        // s[0..i] 计算包含 `0` 的个数
        // s[i..] 计算包含 `1` 的个数
        let sum = count_zeros[i - 1] + count_ones[i];
        max_sum = max_sum.max(sum);
    }
    max_sum
}
}

算法的时间复杂度是 O(n), 空间复杂度是 O(n), 因为引入了两个辅助数组.

1480. 一维数组的动态和 Running Sum of 1d Array

问题描述

这个就是基本的前缀和问题.

#![allow(unused)]
fn main() {
// Prefix sum
pub fn running_sum1(nums: Vec<i32>) -> Vec<i32> {
    debug_assert!(!nums.is_empty());
    let mut prefix = vec![0; nums.len()];
    prefix[0] = nums[0];
    for i in 1..nums.len() {
        prefix[i] = prefix[i - 1] + nums[i];
    }
    prefix
}
}

时间复杂度是 O(n), 空间复杂度是 O(n)

1498. 满足条件的子序列数目 Number of Subsequences That Satisfy the Given Sum Condition

问题描述

TODO(Shaohua):

问题 1501-1600

1518. 换水问题 Water Bottles

问题描述

所谓的兑换空瓶子, 问题就是 商与余数(DivMod) 的问题.

想到这个方向, 就简单了. 但要考虑到几个细节问题:

  • num_bottles 就是被除数
  • num_exchange 就是除数
  • 一次兑换交易, 从空瓶子换成满瓶水, 就是一个整数的除法操作
  • 既然是整数除法, 那就有可能还有余数

div-mod

代码里有更详细的注释:

#![allow(unused)]
fn main() {
pub fn num_water_bottles(num_bottles: i32, num_exchange: i32) -> i32 {
    // 满瓶水的数量
    let mut full_bottles = num_bottles;
    // 已经喝了多少瓶
    let mut count = 0;
    // 空瓶子的数量
    let mut empty_bottles = 0;

    // 当还有满瓶的水, 或者空瓶子的数量大于最小兑换数时, 游戏就可以进行
    while full_bottles > 0 || empty_bottles >= num_exchange {
        // 喝水
        count += full_bottles;
        // 收集空的瓶子
        empty_bottles += full_bottles;
        // 把空瓶子兑换成满瓶的水
        full_bottles = empty_bottles / num_exchange;
        // 可能还会剩下一些空瓶子在本次无法兑换
        empty_bottles %= num_exchange;
    }
    count
}
}

问题 1601-1700

问题 1701-1800

1732. 找到最高海拔 Find the Highest Altitude

问题描述

这个问题可以使用前缀和 prefix sum 的算法, 但是是逆着来的, 给出的是前缀和数组, 我们需要得到 原先的数组.

#![allow(unused)]
fn main() {
// Prefix sum
pub fn largest_altitude1(gain: Vec<i32>) -> i32 {
    debug_assert!(!gain.is_empty());
    let mut last_altitude = 0;
    let mut highest = 0;
    for altitude in gain {
        last_altitude += altitude;
        highest = highest.max(last_altitude);
    }
    highest
}
}

时间复杂度是 O(n), 空间复杂度是 O(1).

1780. 判断一个数字是否可以表示成三的幂的和 Check if Number is a Sum of Powers of Three

问题描述

这个题目比较费脑子, 要反复仔细考虑题目里的细节, 这算是个数学问题.

这个题目可以用下面的数学公式表述出来:

\[ n = a_i \sum_{i=0} 3^{x_i}, a_i \in \{ 0, 1 \} \]

解释一下这个公式:

  • 该整数n, 是有三的多个次幂之和组成
  • 三的某次幂, 比如 3^5, 只可能在这个求和公式中出现0次或者1次

然后, 我们可以反向思考, 如果我们能依次算出 a_i 的值, 并对比它的值是不是0或者1, 就可以最终解决问题了.

如果还不好理解, 就看一下下图: sum-of-exp

基于以上的解释, 就可以写代码了:

#![allow(unused)]
fn main() {
// n 是有穷级数, 计算出级数中各项的系数, 只能是0或者1
pub fn check_powers_of_three1(n: i32) -> bool {
    assert!(n >= 1);
    let mut n = n;
    while n != 0 {
        if n % 3 == 2 {
            return false;
        }
        n /= 3;
    }
    true
}
}

相关问题

问题 1801-1900

1854. 人口最多的年份 Maximum Population Year

问题描述

这是一个计数的问题, 首先想到的就是字典计数.

BTreeMap

计数步骤如下:

  1. 创建字典, 为了找到相同生存人数的最小年份, 我们使用 BTreeMap 来保证年份的有序.
  2. 遍历 logs 中的所有记录, 并遍历每条记录的出生到去世间的所有年份, 将本年度加入到字典中
  3. 遍历有序字典, 找到有最大生存人数的那一年
#![allow(unused)]
fn main() {
use std::collections::BTreeMap;

// Hashmap
// 使用字典计数
pub fn maximum_population1(logs: Vec<Vec<i32>>) -> i32 {
    debug_assert!(!logs.is_empty());
    // 因为要得到最早的年份, 我们使用 BTreeMap 来保证年份有序.
    let mut map = BTreeMap::new();

    for log in logs {
        for year in log[0]..log[1] {
            *map.entry(year).or_default() += 1;
        }
    }

    let mut max_count = 0;
    let mut max_year = 0;
    for (year, count) in map {
        if count > max_count {
            max_count = count;
            max_year = year;
        }
    }
    max_year
}
}

这个算法的时间复杂度是 O(n * m), 其中 n 是人数, m 是生存的年份. 空间复杂度是 O(n), 其中 nlogs 的年份范围.

计数数组

因为年份跨度比较小, 只有100年, 我们可以用栈上的数组来代替 BTreeMap, 其它步骤没有太多变化.

#![allow(unused)]
fn main() {
// 计数
// 使用数组代替字典
pub fn maximum_population2(logs: Vec<Vec<i32>>) -> i32 {
    const MAX_YEARS: usize = 100;
    debug_assert!(!logs.is_empty());
    let start_year: usize = 1950;
    let mut timeline = [0; MAX_YEARS];

    for log in logs {
        for year in log[0]..log[1] {
            timeline[year as usize - start_year] += 1;
        }
    }

    let mut max_count = 0;
    let mut max_year = 0;
    for (year, &count) in timeline.iter().enumerate() {
        if count > max_count {
            max_count = count;
            max_year = year;
        }
    }
    (max_year + start_year) as i32
}
}

这个算法的时间复杂度是 O(n * m), 其中 n 是人数, m 是生存的年份. 空间复杂度是 O(n), 其中 nlogs 的年份范围.

前缀和

这个有些不好考虑, 在构造前缀和数组之前, 我们先构造一个辅助数组. 整个解决步骤如下:

  1. 创建 alive 辅助数组
  2. 遍历 logs 数组
    1. 当一个人出生时, 将 alive 中出生所在年份计数加1, alive[start_year] += 1
    2. 当一个人去世时, 将 alive 中去世所在年份计数减1, alive[end_year] -= 1
  3. 创建 prefix_sum 前缀和数组, 通过遍历 alive 数组
  4. 最后遍历 prefix_sum 数组, 找到生存人数最多的那个年份
#![allow(unused)]
fn main() {
// Prefix Sum
pub fn maximum_population3(logs: Vec<Vec<i32>>) -> i32 {
    let start_year = 1950;
    let end_year = 2050;
    let no_years = end_year - start_year + 1;

    // 构造数组, 用于记录每年增减的人数,
    // 为构造前缀和数组做准备
    let mut alive = vec![0; no_years as usize];
    for log in logs {
        let start_year_index = (log[0] - start_year) as usize;
        let end_year_index = (log[1] - start_year) as usize;
        alive[start_year_index] += 1;
        alive[end_year_index] -= 1;
    }

    // 构造前缀和数组
    let mut prefix_sum = vec![0; alive.len()];
    prefix_sum[0] = alive[0];
    for i in 1..alive.len() {
        prefix_sum[i] = prefix_sum[i - 1] + alive[i];
    }

    // 遍历前缀和数组, 找到最多的人所在的年份
    let mut max_count = 0;
    let mut max_year = 0;
    for (index, count) in prefix_sum.into_iter().enumerate() {
        if count > max_count {
            max_count = count;
            max_year = index as i32 + start_year;
        }
    }
    max_year
}
}

这个算法的时间复杂度是 O(n), 空间复杂度是 O(n), 其中 nlogs 的年份范围.

1893. 检查是否区域内所有整数都被覆盖 Check if All the Integers in a Range Are Covered

问题描述

这个问题有两个思路可以处理.

集合 Set

使用集合来存储区间上的每个点.

步骤如下:

  1. 创建集合
  2. 遍历 ranges 数组, 将每个范围上的所有点位都存储到集合中
  3. 遍历 [left..=right] 区间上的所有点位, 查看它们是否都在集合中
#![allow(unused)]
fn main() {
use std::collections::HashSet;

// Hashset
pub fn is_covered1(ranges: Vec<Vec<i32>>, left: i32, right: i32) -> bool {
    // 将所有的点位存储到集合中
    let mut set = HashSet::new();
    for range in ranges {
        for i in range[0]..=range[1] {
            set.insert(i);
        }
    }

    // 遍历区间 [left..=right] 上的所有点, 查看它们是否都在集合中
    for i in left..=right {
        if !set.contains(&i) {
            return false;
        }
    }
    true
}
}

该算法:

  • 时间复杂度是 O(n m), 其中 n 是范围的个数, 而 m 是最大的范围区间
  • 空间复杂度是 O(n), 其中 n 是范围包含的所有点位个数

合并区间 Merge intervals

这个方法用于计算区间重叠很方便.

其步骤如下:

  1. 先对 ranges 数组进行排序, 依照范围的起始点
  2. 构造合并区间 intervals
    1. 初始化区间值 start = 0, end = 0
    2. 遍历 ranges, 并判断当前区间是否能跟区间值 [start..=end] 拼接在一起, 判定条件是 range[0] <= end + 1
    3. 如果可以, 就只需要移动区间的终点值, end = end.max(range[1])
    4. 如果不行, 就先将当前区间 [start..=end] 加入到 intervals, 然后更新 [start..=end] 区间
  3. 查找 [left..=right] 区间是否在 intervals
#![allow(unused)]
fn main() {
// Merge intervals
pub fn is_covered2(ranges: Vec<Vec<i32>>, left: i32, right: i32) -> bool {
    // 依照起始点对区间进行排序
    let mut ranges = ranges;
    ranges.sort_unstable_by_key(|range| range[0]);

    // 合并区间
    let mut intervals = Vec::new();
    let mut start = 0;
    let mut end = 0;
    for range in ranges {
        if range[0] > end + 1 {
            // 区间无法拼接在一起
            if start <= end {
                intervals.push((start, end));
            }
            start = range[0];
            end = range[1];
        } else {
            // 区间可以拼接在一起
            end = end.max(range[1]);
        }
    }
    if start <= end {
        intervals.push((start, end));
    }

    // 查找区间是否有包含
    // 可以使用二分法查找
    for interval in intervals {
        if interval.0 <= left && right <= interval.1 {
            return true;
        }
    }
    false
}
}

该算法:

  • 时间复杂度是 O(n log(n)), n 是 ranges 中的区间个数
  • 空间复杂度是 O(n), n 是 ranges 内不连接的区间个数

问题 1901-2000

找到数组的中间位置 Find the Middle Index in Array

问题描述

这个问题与 寻找数组的中心下标 Find Pivot Index 完全相同, 我们不再详细解析, 具体可以参考 0724 问题的解法说明.

我们只是简单实了其中一种方法:

#![allow(unused)]
fn main() {
//! 与 0724 问题相同, 我们只写第二种解法

pub fn solution1(nums: Vec<i32>) -> i32 {
    let mut suffix_sum: i32 = nums.iter().sum();
    let mut prefix_sum: i32 = 0;
    for (index, num) in nums.iter().enumerate() {
        suffix_sum -= num;
        if prefix_sum == suffix_sum {
            return index as i32;
        }
        prefix_sum += num;
    }
    -1
}
}

问题 2101-2200

2108. 找出数组中的第一个回文字符串 Find First Palindromic String in the Array

问题描述

这道题与 0125. 验证回文串 Valid Palindrome 基本一致, 只是多了一个循环遍历, 将不再讲解.

靠拢型双指针

#![allow(unused)]
fn main() {
// 双指针法
pub fn first_palindrome1(words: Vec<String>) -> String {
    fn is_palindrome(s: &str) -> bool {
        let bytes = s.as_bytes();
        let mut left = 0;
        let mut right = bytes.len() - 1;
        while left < right {
            if bytes[left] != bytes[right] {
                return false;
            }
            left += 1;
            right -= 1;
        }
        true
    }

    for word in &words {
        if is_palindrome(word) {
            return word.clone();
        }
    }
    String::new()
}
}

反转字符串

#![allow(unused)]
fn main() {
// 反转字符串
pub fn first_palindrome2(words: Vec<String>) -> String {
    fn is_palindrome(s: &str) -> bool {
        s.chars().rev().collect::<String>() == s
    }
    for word in &words {
        if is_palindrome(word) {
            return word.clone();
        }
    }
    String::new()
}
}

相关问题

2119. 反转两次的数字 A Number After a Double Reversal

问题描述

仔细分析这个问题, 可以发现一个细节: 只要在反转时不丢弃任何一个数字, 才能保证两次反转后的整数值不变.

那么什么情况下会丢弃数字呢? 只有个位数为0的整数, 在反转一次时, 会将作为前缀的0给丢弃掉. 其它类型的整数, 在反转两次后, 一定保持不变的.

当然了, 整数本身为0, 是一个特属情况.

取得整数个位数字的方法也很简单, 只需要做一次 %10, 将十进制的整数右移一位即可.

get-unit-digit

最终的代码如下:

#![allow(unused)]
fn main() {
pub fn is_same_after_reversals(num: i32) -> bool {
    if num == 0 {
        return true;
    }
    num % 10 != 0
}
}

2401-2500

2485. 找出中枢整数 Find the Pivot Integer

问题描述

这个问题的解法就比较多了.

前缀和 Prefix Sum

根据问题中的描述, 可以直接想到的方法就是前缀和.

处理步骤如下:

  1. 构造 [1..=n] 的前缀和数组
  2. 遍历前缀和数组, 如果左侧部分等于右侧部分, 就返回, prefix * 2 == total_sum + i as i32
  3. 否则没有找到, 就返回 -1

代码实现如下:

#![allow(unused)]
fn main() {
// Prefix Sum
pub fn pivot_integer1(n: i32) -> i32 {
    let len = (n + 1) as usize;

    // 构造前缀和数组
    let mut prefix_sum = vec![0_i32; len];
    prefix_sum[0] = 0;
    prefix_sum[1] = 1;
    for i in 2..len {
        prefix_sum[i] = prefix_sum[i - 1] + i as i32;
    }

    let total_sum = prefix_sum[len - 1];

    // 遍历前缀和数组
    for (i, prefix) in prefix_sum.into_iter().enumerate() {
        // 满足左侧之和等于右侧之后
        if prefix * 2 == total_sum + i as i32 {
            return i as i32;
        }
    }

    -1
}
}

该算法的时间复杂度是 O(n), 空间复杂度是 O(n).

双指针法

步骤如下:

  1. 构造两个指针, 分别代表左侧之和与右侧之和
    1. 左侧之和初始化为0
    2. 右侧之和被初始化为 n * (n + 1) /2
  2. 遍历 1..=n, 然后判断左侧之和与右侧之和的关系
    1. 如果 left_sum + i == right_sum, 说明找到了该位置, 直接返回
    2. 否则更新左侧之和与右侧之和
    3. 如果 left_sum > right_sum, 说明没有找到合适的位置, 直接返回 -1

该算法的实现如下:

#![allow(unused)]
fn main() {
// Two pointers
pub fn pivot_integer2(n: i32) -> i32 {
    let mut left_sum = 0;
    let mut right_sum = n * (n + 1) / 2;

    // 注意遍历的范围, 是 [1..=n]
    for i in 1..=n {
        if left_sum + i == right_sum {
            return i;
        }
        left_sum += i;
        right_sum -= i;
        if left_sum > right_sum {
            break;
        }
}

该算法的时间复杂度是 O(n), 空间复杂度是 O(1).

平方根法

这个方法的原理还不太清楚, 先计录一下.

#![allow(unused)]
fn main() {
// Sqrt
pub fn pivot_integer3(n: i32) -> i32 {
    let sum = n * (n + 1) / 2;
    let sqrt = (sum as f64).sqrt() as i32;
    if sqrt * sqrt == sum {
        sqrt
    } else {
        -1
    }
}
}

该算法的时间复杂度是 O(1), 空间复杂度是 O(1).

2501-2600

2574. 左右元素和的差值 Left and Right Sum Differences

问题描述

这是一个简单的前缀和 prefix sum 问题.

Prefix sum

处理思路如下:

  1. 计算 left_sum, 从左到右遍历原数组, 并计算前缀和, left_sum[i + 1] = left_sum[i] + nums[i]
  2. 计算 right_sum 从左到右遍历原数组, 并计算前缀和, right_sum[i - 1] = right_sum[i] + nums[i]
  3. 计算遍历两个数组, 并计算 (left_sum[i] - right_sum[i]).abs(), 就得到了答案
#![allow(unused)]
fn main() {
// Prefix Sum
pub fn left_right_difference1(nums: Vec<i32>) -> Vec<i32> {
    debug_assert!(!nums.is_empty());

    let len = nums.len();
    let mut left_sum = vec![0; len];
    left_sum[0] = 0;
    // 从左向右遍历
    for i in 0..(len - 1) {
        left_sum[i + 1] = left_sum[i] + nums[i];
    }
    //println!("left sum: {left_sum:?}");

    let mut right_sum = vec![0; len];
    right_sum[0] = 0;
    // 从右向左遍历
    for i in (1..=(len - 1)).rev() {
        right_sum[i - 1] = right_sum[i] + nums[i];
    }
    //println!("right sum: {right_sum:?}");

    left_sum
        .into_iter()
        .zip(right_sum)
        .map(|(left, right)| (left - right).abs())
        .collect()
}
}

该算法的时间复杂度是 O(n), 空间复杂度是 O(n).

2801-2900

2848. 与车相交的点 Points That Intersect With Cars

问题描述

这个问题的解法就比较多了.

哈稀表

确切来说是 HashSet, 我们用集合来统计每辆车占用的点位, 最后计算集合中点的个数就行.

操作步骤如下:

  1. 创建统计用的集合
  2. 遍历所有车辆, 将每个车辆占用的点位区间上的所有点, 都加入到集合中
  3. 集合的长度就是所有的点位数
#![allow(unused)]
fn main() {
// Hash Map
pub fn number_of_points1(nums: Vec<Vec<i32>>) -> i32 {
    debug_assert!(!nums.is_empty());
    let mut set = HashSet::new();
    for num in nums {
        debug_assert!(num.len() == 2);
        for i in num[0]..=num[1] {
            set.insert(i);
        }
    }
    set.len() as i32
}
}

该算法的时间复杂度是 O(n), 空间复杂度是 O(n).

Bitset

这个是对上述方法的优化, 因为给定的点数比较少, 我们也可以直接使用 Bitset 来统计点位数, 为了实现简单, 我们直接使用 [false; 101] 来作为 bitset.

#![allow(unused)]
fn main() {
// Bit Set
pub fn number_of_points2(nums: Vec<Vec<i32>>) -> i32 {
    debug_assert!(!nums.is_empty());
    let mut bitset = vec![false; 101];
    for num in nums {
        for i in num[0]..=num[1] {
            debug_assert!(i >= 0);
            bitset[i as usize] = true;
        }
    }
    bitset.into_iter().filter(|x| *x).count() as i32
}
}

该算法的时间复杂度是 O(n), 空间复杂度是 O(n).

合并区间 Merge intervals

这个方法是 0056. Merge intervals 的解法.

因为给定的区间是无序的, 我们先以每个区间的起始点来对它进行排序, 之后再统计.

步骤如下:

  1. 对所有车辆所占用的区间的起始点进行排序
  2. 创建一个动态数组 intervals, 用来存储不重叠的区间
  3. 遍历 nums 中的所有数对, 然后合并有重叠的区间, 并将不重叠的区间存储到 intervals 数组中
    1. 如果有重叠, 只需要更新区间的终点值即可
    2. 如果没有重叠, 则需要把之间的区间存到 intervals 数组, 并同时更新起点和重点
  4. 遍历 intervals 数组, 统计所有不重叠区间占用的点数
#![allow(unused)]
fn main() {
// Merge Intervals
// See leetcode #0056
pub fn number_of_points4(nums: Vec<Vec<i32>>) -> i32 {
    debug_assert!(!nums.is_empty());
    // 先对间隔的起始点排序
    let mut nums = nums;
    nums.sort_unstable_by_key(|num| num[0]);

    let mut intervals = Vec::with_capacity(nums.len());
    let mut start = nums[0][0];
    let mut end = nums[0][1];
    for num in nums.into_iter().skip(1) {
        if num[0] > end {
            // 说明有间隔, 要移动起始点和终点
            intervals.push((start, end));
            start = num[0];
            end = num[1];
        } else {
            // 没有间隔, 只移动终点
            end = end.max(num[1]);
        }
    }
    // 最后一个间隔值
    intervals.push((start, end));
    let mut count = 0;
    for (start, end) in intervals {
        count += end - start + 1;
    }
    count
}
}

该算法的时间复杂度是 O(n log(n)), 空间复杂度是 O(n).

3001-3100

3028. 边界上的蚂蚁 Ant on the Boundary

问题描述

这是一个经典的前缀和数组问题.

解题思路如下:

  1. 先遍历 nums 数组, 构造出前缀和数组 positions
  2. 遍历 positions 数组, 统计里面数值 0 出现的次数

该算法的实现如下:

#![allow(unused)]
fn main() {
// Prefix Sum
pub fn return_to_boundary_count1(nums: Vec<i32>) -> i32 {
    debug_assert!(!nums.is_empty());

    let mut positions = vec![0; nums.len()];
    positions[0] = nums[0];
    for i in 1..nums.len() {
        positions[i] = positions[i - 1] + nums[i];
    }
    positions.into_iter().filter(|&x| x == 0).count() as i32
}
}

该算法的时间复杂度是 O(n), 空间复杂度是 O(n).

上面的算法可以做一些简化, 因为是一次性获取结果, 其是是不需要将中间值存入到 positions 数组的, 在遍历 nums 数组时可以直接更新 zeros_count 计数值. 算法实现如下:

#![allow(unused)]
fn main() {
// Prefix Sum
// 不保存中间值到数组
pub fn return_to_boundary_count2(nums: Vec<i32>) -> i32 {
    debug_assert!(!nums.is_empty());

    let mut zeros_count = 0;
    let mut last_position = 0;
    for num in nums {
        last_position += num;
        if last_position == 0 {
            zeros_count += 1;
        }
    }
    zeros_count
}
}

该算法的时间复杂度是 O(n), 空间复杂度是 O(1).

参考资料

  • "Algorithms", 作者 Robert Sedgewick, Kevin Wayne, 作为算法的入门课
  • "Data Structures and Algorithms in C++", 第4版, 作者 Mark Allen, 数据结构讲的比较好
  • "Algorithm Design", 作者 Jon Kleinberg, Éva Tardos, 算法分析的核心课程
  • "Introduction to Algorithms", 作者 Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein