数据结构与算法 - Rust 语言实现
使用 Rust 语言实现所有的数据结构与算法.
本文档包括了以下几个部分的内容:
- 第一部分: 数据结构
- 第二部分: 算法
- 第三部分: 专题
- 第四部分: leetcode 题解
- 第五部分: 华为 OD 机试题解
反馈问题
欢迎 反馈问题, 或者提交 PR.
搭建本地环境
想在本地搭建本文档的环境也是很容易的, 这些文档记录以 markdown 文件为主, 用 mdbook 生成网页:
- 用cargo来安装它:
cargo install mdbook mdbook-linkcheck mdbook-pagetoc
- 运行
mdbook build
命令, 会在book/
目录里生成完整的电子书的网页版本. - 使用
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
- 对其中的任意元素进行访问的时间一致, 支持随机访问
数组基本的布局如下图如示:
根据元素分布方式, 可以将数组分类为:
- 一维数组 (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
反转数组, 就是将数组中各元素转换到它相反的位置:
- 第一个位置的元素移到最后一位
- 第二个位置的元素移到倒数第二位
- 依次类推
得到的结果如下图所示:
操作过程
根据反转数组的描述, 使用靠拢型双指针法遍历整个数组, 并交换元素的值.
操作步骤如下图所示:
代码实现
#![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: 使用临时数组, 拷贝一份
操作过程如下:
- 将
arr[k..n]
存储到临时数组 - 将
arr[0..k]
存储到临时数组 - 将临时数组中的元素拷贝回原数组
这个方法的时间复杂度是 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: 三次反转法
操作过程如下:
- 将
arr[k..n]
进行反转 - 将
arr[0..k]
进行反转 - 将
arr[..]
进行反转
这个方法是在原地操作的, 其时间复杂度是 O(n)
, 空间复杂度是 O(1)
.
流程如下图所示:
代码如下:
#![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: 一步到位
所谓的一步到位法, 就是先计算好每个元素在旋转后的新位置, 然后依次转移每一个元素, 一步到位; 每个元素只移动一次.
操作过程如下:
- 计算数组中元素个数
n
与偏移量k
的最大公约数divisor
- 然后从 0 循环到
divisor
, 把数组中的元素分成以k
为步长, 组成的集合; 如果索引值超过了数组长度, 就取余 - 在循环体内部, 将集合中的第一个元素存到临时变量
- 依次将集合中的后一元素移动前一个元素
- 将临时变量存储到集合中的最后一个元素
- 最终将该集合中所有元素依次移位
这个方法是在原地操作的, 其时间复杂度是 O(n)
, 空间复杂度是 O(1)
.
流程如下图所示:
#![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];
算法实现如下:
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}
这个矩阵用数组存放, 效果如下图:
这种存储方式的特点是:
- Row major 风格
- 数组中元素的排序方法是
- 从头到尾以行编号递增
- 相同行编号时, 以列编号递增
- 即整体上行编号有序递增, 整体上列编号无序, 但局部上列编号递增
- 查找矩阵中某个节点的值时的性能是
O(log(m) * log(n))
, 其中m
和n
是矩阵中非 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}
用链表来记录的话, 其结构如下图所示:
这种存储方式的特点是:
- 同样是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<'a, T: IsZero> ExactSizeIterator for Iter<'a, 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<'a, T: IsZero> ExactSizeIterator for IterMut<'a, 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}
这个矩阵用数组存放, 效果如下图:
这种存储方式的特点是:
- Row major 风格
- 分两层链表来存储
- 第一层是行级链表, 存储非空行, 且以行号递增排序
- 第二层, 在每个行链表节点中, 存储非空列的链表, 且以列号递增排序
- 查找矩阵中某个节点的值时的性能是
O(m * n)
, 其中m
和n
是矩阵中非 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 中节点是按照 key 的顺序进行存储的, 而我们选用 (row, column) 作为 key, 这样
- 首先以行号递增排序
- 如果行号相同, 以列号递增排序
- 查找/插入/删除矩阵中某个节点的值时的性能是
O(log(m * n))
, 其中m
和n
是矩阵中非 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<'a> Iterator for BitSetIter<'a> { 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), 它由一系列的节点组成; 节点内部保存着元素的值, 节点之间使用指针或者引用引连, 可以顺着指针/引用找到下个节点在内存中的位置.
链表的类型
根据链表的结构, 有这几种类型:
- 单链表 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
在单链表中, 每个节点包括一个指针, 指向下个节点.
特点:
- 只能从链表头部单向地遍历整个链表
- 每个节点只需要存储一个指针元素, 可以节省一些内存空间
单链表的结构如下图所示:
C语言中对应的结构体声明如下:
// 单链表
struct singly_list_s {
int value;
struct singly_list_s* next;
};
双链表 Doubly Linked List
在双链表中, 每个节点持有两个指针, 分别指向它的前一个节点以及后一个节点.
特点:
- 可以向前和向后双向遍历整个链表
- 每个节点要存储两个指针, 占用更多的内存空间
双链表的结构如下图所示:
C语言中对应的结构体声明如下:
// 双链表
struct doubly_list_s {
int value;
struct doubly_list_s* previous;
struct doubly_list_s* next;
};
环状链表 Circular Linked List
与单链表不同, 环状链表的最后一个节点指向链表的第一个节点, 形成一个环.
特点是:
- 遍历环状链表一周后, 可以回到起始节点
环状链表的结构如下图所示:
C语言中对应的结构体声明如下:
// 环状链表
struct circular_list_s {
int value;
struct circular_list_s* next;
};
环状双链表 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
, 链表的指针
指向该节点, 然后该节点再指向链表的真正存放数据元素的第一个节点.
特点:
- 支持向后单向遍历节点
- 更方便针对链表节点的操作
该链表的结构如下图所示:
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;
};
记录个人信息的列表, 可以基于人的姓名排序, 也可以基于年龄排序, 其结构图如下所示:
或者展示稀疏矩阵:
- | 0 | 1 | 2 |
---|---|---|---|
0 | 0 | 5 | 0 |
1 | 0 | 0 | 0 |
2 | 20 | 0 | 10 |
3 | 6 | 0 | 0 |
使用以下数据结构:
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;
};
这样的稀疏矩阵可以同时基于行号和列号进行线性查找, 比较方便. 其结构图如下所示:
或者表示多层链表 Multi-level linked list, 又称十字链表法 Orthogonal linked list.如下图所示:
还有一种简化了的, 称为 List of lists (LIL), 这种的, 索引方式要简单些. 如下图所示:
Unrolled Linked List
上面的链表中, 每个节点都只存储一个元素值, 我们也可以在一个节点中存储多个元素值. 这种链表就是 Unrolled Linked List.
特点:
- CPU 缓存更友好, 提高缓存命中率
- 访问相邻的元素的性能更好
其结构图如下所示:
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) 中每个节点包含两个指针, 分别指向左右相邻的节点.
其结构如下图所示:
双链表的优点:
- 反转双向链表非常容易
- 它可以在执行过程中轻松分配或重新分配内存
- 与单链表一样, 它是最容易实现的数据结构
- 此双向链表的遍历是双向的, 这在单链表中是不可能的
- 与单链表相比, 删除节点很容易. 单链表删除需要指向要删除的节点和前一个节点的指针, 但在双向链表中, 它只需要要删除的指针. 与其他数据结构 (如数组) 相比, 双向链表的开销较低
- 可用于实现图算法 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<'a, T> DoubleEndedIterator for Iter<'a, 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<'a, T> ExactSizeIterator for Iter<'a, 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<'a, T> DoubleEndedIterator for IterMut<'a, 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<'a, T> ExactSizeIterator for IterMut<'a, 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]); } } }
环状双链表
与上文提到的双链表相比, 环状双向链表只是连接了链表的头部节点和尾部节点, 其它操作都差不多.
其结构如下图所示:
算法实现
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) 的顺序进行操作. 即先放入栈的元素离栈底较近, 出栈顺序比较晚, 后放入栈的元素离栈顶较近, 出栈顺序较早.
栈的基本结构如下图所示:
跟据栈的大小可以分类为:
- 固定大小的栈: 一旦初始化完成, 便不允许调整栈的大小. 如果当前栈已满, 再向栈顶加入新元素时就会触发 栈已满的错误; 如果当前栈是空的, 调用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, 并将新元素加入到栈顶的位置
如果是动态栈, 不受容量限制, 那这个函数就没有返回值, 也不存在栈满的问题.
出栈 pop()
元素出栈顺序跟其入栈顺序是相反的.
从栈顶移出元素:
- 如果栈已空, 就直接返回
None
- 将栈顶的索引值
top
减去1, 并返回旧的栈顶元素
返回栈顶的元素 top()
返回栈顶元素:
- 返回之前先检查栈是否为空, 如果为空, 就直接返回栈空的错误
- 返回当前的栈顶元素, 对栈不做任何改动
检查栈是否为空 is_empty()
- 检查栈里的
top
的值 - 如果
top == 0
, 则说明栈为空, 返回true
- 否则栈中存有元素, 不为空, 返回
false
检查栈中当前的元素个数 len()
直接返回 len
属性
检查栈的容量 capacity()
直接返回 capacity
属性
栈的实现
使用数组实现
使用数组实现的栈结构, 它能保存的元素个数是固定的, 需要在初始化栈时指定栈的容量.
这里, 我们使用 Box<[Option<T>]>
用于指示数组中是否存储了元素, 如果它为 None
则表示在位置没有元素.
另外一种实现方式是 Box<[T]>
, 并且要求类型 T
实现 Clone
trait.
#![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) } } }
栈的应用
栈是一个线性的数据结构. 它在编程领域有广泛的使用. 比如操作系统会为每个线程分配一个函数调用栈, 用于保存函数内的局部变量.
常见的栈的应用有:
- 函数调用: 用于记录函数返回地址, 当被调用函数执行完后可以将返回值正确还回给函数调用处
- 递归: 递归函数的调用, 通常可以将它们转换成迭代的形式, 这时可以利用栈来存放每次递归调用时的值
- 语法解析: 可以利用栈来检验编程语言中表达式的语法
- 表达式求值: 可以用栈来实现对后缀表达式的求值
单调栈 Monotonic Stack
队列 Queues
队列是计算机科学里的基础的概念, 它用于以一定顺序存储和管理数据.
它遵循"先进先出" (First In First Out, FIFO) 的原则, 即先进入到队列里的元素会先出队列. 它是两端开口的线性数据结构 (linear data structure),
基本的操作如下图所示:
队列的分类
根据队列可以容纳的元素个数不同, 可以被分为:
- 静态队列, 或者固定队列: 即队列在初始化时就指定它的容量, 队列中的元素个数不同超过该容量, 否则就出队 (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.
环形缓冲区是线性数据结构, 通常由数组来实现, 如下图所示:
将尾部与头部相连, 组成一个环形索引, 逻辑上的关系如下图所示.:
所以才称为环形缓冲区.
环形缓冲区实现的是单生产者-单消费者模式 (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
参考
- ConcurrentQueue
- A Fast General Purpose Lock-Free Queue for C++
- A Fast Lock-Free Queue for C++
- Detailed Design of a Lock-Free Queue
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, 找到第二大的元素, 并放在数组右侧第二个位置
- 直到没有元素需要被交换, 整个数组变得有序
下面以 arr = [9, 4, 1, 7];
为例来进行演示.
第一阶段, 从左到右遍历数组, 找到最大的元素 9
, 并将它放在数组最右侧.
第一阶段, 从左到右遍历数组, 找到最大的元素 7
, 并将它放在数组右侧第二个位置.
第一阶段, 从左到右遍历数组, 发现数组已排序完成.
实现冒泡排序算法
#![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, 就直接返回
- 将最大的元素移到数组最右侧
- 递归调用冒泡排序, 但忽略数组的最右侧元素
递归形式的冒泡排序算法需要额外占用 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
插入排序实现方法比较简单. 它一次排序一个元素, 将数组分成两部分, 左侧部分是有序的, 右侧部分是待排序的.
插入排序的步骤
- 从第二个元素开始遍历数组, 因为数组中的第一个元素是有序的
- 将第二个元素与第一个元素比较, 如果比第一个元素小, 就交换两者的位置
- 将第三个元素与第二个位置的元素比较, 如果比它小, 就交换位置, 并重复第2步
- 继续以上的步骤, 将无序元素与前面的有序的元素进行比较以及交换位置
- 重复操作, 直到整个数组都是有序的
第一阶段, 将第二个元素 4
与第一个元素 9
进行比较, 并交换位置:
第二阶段, 将第三个元素 1
与前面的元素比较, 并交换位置:
第三阶段, 将第四个元素 7
与前面的元素比较并交换位置:
插入排序的实现
#![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; } } } } }
递归实现插入排序
根据上面的描述, 插入排序会将数组分成两部分, 左侧部分是已经排好序的, 右侧部分是待排序的. 现在用递归的形式重新实现这个步骤:
- 对于第
k
个元素, 先将list[0..k]
进行递归排序 - 然后将第
k
个元素与前面已经排序好的list[0..k]
进行比较并交换位置, 以便让它放在合适的位置 - 这样的话, 整个数组最终就会变成有序的
#![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
选择排序的逻辑很简单, 将数组分成两部分:
- 左侧部分是排序好的, 按顺序放着较小的元素
- 右侧部分是未排序的, 放着较大的元素
选择排序的步骤
- 遍历数组, 找到最小的元素, 让它与最左侧元素交换
- 遍历数组中剩下的元素, 找到最小的元素, 让它与最左侧第二位元素交换
- 重复上面的步骤, 直到所有元素都有序排列
我们以 arr = [9, 4, 1, 7];
为例进行演示:
首先找到最小的元素 1
, 把它与最左侧元素相交换:
第二阶段, 找到剩下元素中最小的元素 4
, 把它与左侧第二位相交换:
第三阶段, 找到最小的元素 7
, 把它与左侧第三个元素相交换:
到达了数组的最右侧, 所有元素都已排好序.
选择排序的代码实现
#![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; } } } }
选择排序的特点
- 即使数组中的元素基本排序好, 也需要遍历所有元素并比较大小, 这种情况下效率较低, 依然需要
n^2 / 2
次比较操作 以及n
次交换, 平均时间复杂度是O(n log(n))
, 空间复杂度是O(1)
- 在所有排序算法中, 选择排序移动元素的次数最少, 每个元素最多只移动一次, 就可以移到最终位置; 这个算法比较适合那种比较元素时的成本低, 但移动元素成本比较高的情况 (比如, 移动文件中的内容)
- 选择排序是原地排序 (in-place sort)
- 选择排序是 in-adaptive sort
- 默认实现的选择排序算法是不稳定排序 (unstable), 但优化后的算法可以实现稳定排序 (stable sort)
归并排序 Merge sort
归并排序是 分治算法 的经典实现. 它将数组分成较小的数组并排序, 然后再将它们合并在一起, 得到的数组就是有序的了.
归并排序的步骤
默认实现的递归排序是自顶向下(top-down merge sort)的, 即将整个数组递归分隔.
- 分隔 divide: 将数组递归分成两部分子数组, 直到每部分只剩下一个元素为止
- 攻克 conquer: 使用分治算法排序每个子数组
- 合并 merge: 将排序好的子数组有序合并在一起
第一阶段: 将数组递归分隔 (partition) 成左右两部分:
第二阶段, 将子数组合并在一起:
归并排序的实现
#![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 个元素比较并合并在一起
- 重复以上过程, 直到所有元素合并在一起
下面的流程图展示了一个简单的操作示例:
对应的代码实现如下:
#![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()
函数中引入了更多的比较操作, 其性能可能更差
原地归并排序
原地归并排序, 是替代了辅助数组, 它使用类似插入排序的方式, 将后面较大的元素交换到前面合适的位置. 尽管省去了辅助数组, 但是因为移动元素的次数显著境多了, 其性能表现并不好.
下面的流程图展示了一个原地归并排序的示例:
#![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 的实现
#![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 的选择.
- 我们选取数组的最后一个元素作为基准值 pivot, 分隔数组为左右两部分
- 使用变量
i
标记当前比基准值大的元素位置 - 遍历数组, 把比基准值小的元素交换到
i
的左侧, 比基准值大的元素留在元素i
的右侧 - 最后, 把元素
i
与数组最右侧的基准值元素交换位置, 这样就把基准值放在了它的最终位置
- 使用变量
- 将数组分成两部分, 左侧部分的元素值都比基准值小, 右侧部分比基准值大
- 然后递归调用快速排序算法, 对左右两侧子数组进行排序
下面以 arr = [1, 8, 3, 9, 4];
为例子展示如何对数组分区.
首先选择最后一个元素 4
作为基准值 pivot.
将第一个元素 1
与基准值比较, 它比基准值小, 就需要交换元素 swap(i, j)
, 并将索引 i
右移一位:
将第二个元素 8
与基准值比较, 它比基准值大, 就什么都不做:
将第三个元素 3
与基准值比较, 它比基准值小, 就需要交换元素 swap(i, j)
, 并将索引 i
右移一位:
将第四个元素 9
与基准值比较, 它比基准值大, 就什么都不做:
最后一步, 将基准值 pivot 元素与当前的元素 i
进行交换, 这样的话 pivot 就被移动到了它的最终位置:
快速排序的实现
默认使用最后一个元素作为基准值 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, 这样就减少了元素交换的次数.
希尔排序的步骤
- 初始化间隔值
h = len / 3
- 使用插入排序法, 将
arr[h..]
与arr[..h]
间的元素进行排序, 使用插入排序法, 但两个待比较的元素的间隔是h
, 而不是默认的1
, 这一步很重要, 它有助于减少元素的移动次数 - 减少间隔值,
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
, 将它与第一个元素进行比较并交换位置:
第二阶段, 找到第三个元素1
, 将它与左侧的元素进行比较并换换位置:
第三阶段, 找到第三个元素7
, 将它与左侧的元素进行比较并换换位置:
侏儒排序的实现
#![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), 这类算法只适合整数和定长的字符串.
桶排序也是一种线性排序方法. 它将元素分配到多个桶中, 然后对每个桶单独进行排序.
桶排序的步骤
- 根据原数组中元素的数值范围, 将数组分成
m
个桶, 每个桶将存放一定数值区间的元素, 而且这些数值区间有序不重叠 - 按顺序遍历数组, 将元素按数值大小放到目标桶中, 每个桶会存放相近或者相同的元素
- 使用插入排序等算法对每个桶排序
- 按照桶的顺序, 将每个桶中的元素依次存储到原数组
桶排序的实现
#![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.
线性查找的步骤
- 从数组的第一个元素开始遍历整个数组
- 将当前元素与目标元素进行比较
- 如果当前元素与相等, 就终止循环并返回当前元素的索引值
- 如果不相等, 就移到数组中的下一个元素
- 重复第2-4步, 直到数组的尾部
- 如果到达数组尾部后, 仍然没有找到想要的元素, 就返回没有找到(比如用
-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
二分查找的步骤
实现二分查找法
二分查找法的特点
递归实现二分查找法
没有找到元素时, 返回期望的位置
二分查找法的边界值
二分查找相关的问题列表
容易
- 0035. 搜索插入位置 Search Insert Position
- 0069. x 的平方根 Sqrt(x)
- 0278. 第一个错误的版本
- 0349. 两个数组的交集 Intersection of Two Arrays
- 0374. 猜数字大小 Guess Number Higher or Lower
- 0704. 二分查找 Binary Search
- 0744. 寻找比目标字母大的最小字母 Find Smallest Letter Greater Than Target
TODO:
中等
- 0034. 在排序数组中查找元素的第一个和最后一个位置 Find First and Last Position of Element in Sorted Array
- 0074. 搜索二维矩阵 Search a 2D Matrix
- 0153. 寻找旋转排序数组中的最小值 Find Minimum in Rotated Sorted Array
- 0162. 寻找峰值 Find Peak Element
- 0167. 两数之和 II - 输入有序数组 Two Sum II - Input Array Is Sorted
- 0240. Search a 2D Matrix II Search a 2D Matrix II
- 0287. 寻找重复数 Find the Duplicate Number
- 0532. 数组中的数对 K-diff Pairs in an Array
- 0852. 山脉数组的峰顶索引 Peak Index in a Mountain Array
- 1498. 满足条件的子序列数目 Number of Subsequences That Satisfy the Given Sum Condition
TODO:
- 29. 两数相除
- 33. 搜索旋转排序数组 Search in Rotated Sorted Array
- 378. 有序矩阵中第K小的元素
- 436. 寻找右区间
- 454. 四数相加 II
- 792. 匹配子序列的单词数
- 1004. Max Consecutive Ones III
- 2817. Minimum Absolute Difference Between Elements With Constraint
困难
TODO:
三元查找 Ternary Search
Jump Search
Interpolation Search
Exponential Search
标准库中二分查找法的实现
位运算 Bitwise Algorithms
比特位操作函数表:
A | B | A OR B | A AND B | A XOR B | NOT A |
---|---|---|---|---|---|
1 | 1 | 1 | 1 | 0 | 0 |
1 | 0 | 1 | 0 | 1 | 0 |
0 | 1 | 1 | 0 | 1 | 1 |
0 | 0 | 0 | 0 | 0 | 1 |
- BitOr,
|
- BitAnd,
&
- BitXor,
^
- BitNot,
~
- 左移,
<<
- 右移,
>>
对自己异或操作结果为0
这个问题考察的是比特位异或操作中的一个重要特性: A XOR A == 0
.
我们可以利用这个特性, 遍历数组中的每一项, 然后计算异或值, 最后的结果就是那个单值.
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
, 移动慢指针指向下一个元素.
当条件不满足时, 慢指针不动.
具体的过程看下图:
相关问题
靠拢型双指针问题
靠拢型双指针是指使用两个指针从左右两端访问数组, 往中间靠拢直到重叠.
靠拢型指针的一般步骤:
- 初始化左右两个指针, 分别指向数组的左右两端 (left, right)
- 开始遍历数组, 循环中止的条件就是两个指针重叠 (
left == right
) - 根据题目要求, 选中左右两个指针中的一个, 往中间靠靠拢 (
left += 1
或者right -= 1
, 另一个指针不动 - 直到循环中止
Dutch National Flag, DNF
这是上面方法的一个变形, 可以查看问题 0075. 颜色分类 Sort Colors, 这个方法用于实现三路分区 (three-way partition).
TODO(Shaohua): Add more description
相关问题
参考
并行双指针
这也是一类双指针问题. 用两个指针分别遍历两个数组(或者链表).
- 初始化两个指针, 分别指向两个数组的头部元素
- 如果条件成立, 就同时向右(高位)移动两个指针; 否则, 只移动其中一个, 比如
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 问题分类
这一节, 分别基于问题所属的标签, 和问题编号, 列出问题, 方便索引.
目前采用的刷题顺序:
- 数学
- 比特位操作
- 双指针
- 数组
- 链表
- 哈稀表
- 字符串
- 栈
- 队列
- 树
- 回溯
- 贪心
- 动态规划
- 图
困难程度
难度 | 知识点 |
---|---|
入门 | 数组, 字符串, 链表, 排序 |
简单 | 栈, 队列, 哈希表, 双指针 |
中等 | 二叉树, 堆, 单调栈, 滑动窗口, 二分, 位运算 |
困难 | DP, DFS, BFS, 回溯, 贪心, 并查集, 前缀树 |
数组相关的问题列表
容易
TODO:
- 414. 第三大的数
- 581. 最短无序连续子数组
- 605. 种花问题
- 628. 三个数的最大乘积
- 643. 子数组最大平均数 I
- 665. 非递减数列
- 674. 最长连续递增序列
- 697. 数组的度
- 717. 1比特与2比特字符
- 747. 至少是其他数字两倍的最大数
- 830. 较大分组的位置
- 840. 矩阵中的幻方
- 849. 到最近的人的最大距离
- 888. 公平的糖果交换
- 914. 卡牌分组
- 941. 有效的山脉数组
- 989. 数组形式的整数加法
- 1089. 复写零
- 1128. 等价多米诺骨牌对的数量
中级
TODO:
矩阵 Matrix
简单
中等
困难
前缀和数组 (Prefix Sum Array) 相关的问题列表
简单
- 0303. 区域和检索 - 数组不可变 Range Sum Query - Immutable
- 0724. 寻找数组的中心索引
- 1422. 分割字符串的最大得分 Maximum Score After Splitting a String
- 1480. 一维数组的动态和 Running Sum of 1d Array
- 1732. 找到最高海拔 Find the Highest Altitude
- 1854. 人口最多的年份 Maximum Population Year
- 1893. 检查是否区域内所有整数都被覆盖 Check if All the Integers in a Range Are Covered
- 1991. 找到数组的中间位置 Find the Middle Index in Array
- 2485. 找出中枢整数 Find the Pivot Integer
- 2574. 左右元素和的差值 Left and Right Sum Differences
- 2848. 与车相交的点 Points That Intersect With Cars
- 3028. 边界上的蚂蚁 Ant on the Boundary
中等
困难
双指针相关的问题列表
容易
- 0026. 删除有序数组中的重复项 Remove Duplicates from Sorted Array
- 0088. 合并两个有序数组 Merge Sorted Array
- 0125. 验证回文串 Valid Palindrome
- 0349. 两个数组的交集 Intersection of Two Arrays
- 0350. 两个数组的交集 II Intersection of Two Arrays II
- 0485. 最大连续1的个数 Max Consecutive Ones
- 0680. 验证回文串 II Valid Palindrome II
- 0925. 长按键入 Long Pressed Name
- 0977. 有序数组的平方 Squares of a Sorted Array
- 2108. 找出数组中的第一个回文字符串 Find First Palindromic String in the Array
TODO:
- 27. Remove Element
- 160. 相交链表
- 234. 回文链表
- 2511. Maximum Enemy Forts That Can Be Captured
- 2540. Minimum Common Value
中等
- 0005. 最长回文子串 Longest Palindromic Substring
- 0011. 盛最多水的容器 Container With Most Water
- 0015. 三数之和 3Sum
- 0016. 最接近的三数之和 3Sum Closest
- 0031. 下一个排列 Next Permutation
- 0075. 颜色分类 Sort Colors
- 0080. 删除排序数组中的重复项 II Remove Duplicates from Sorted Array II
- 0167. 两数之和 II - 输入有序数组 Two Sum II - Input Array Is Sorted
- 0532.数组中的数对 K-diff Pairs in an Array
TODO:
- 142. 环形链表 II
- 148. Sort List
- 443. 压缩字符串
- 524. 通过删除字母匹配到字典里最长单词
- 986. 区间列表的交集
- 1498. 满足条件的子序列数目 Number of Subsequences That Satisfy the Given Sum Condition
- 1850. Minimum Adjacent Swaps to Reach the Kth Smallest Number
- 2410. Maximum Matching of Players With Trainers
滑动窗口相关的问题列表
简单
中等
- 0003. 无重复字符的最长子串 Longest Substring Without Repeating Characters
- 1004. 最大连续1的个数 III Max Consecutive Ones III
TODO:
困难
二分查找相关的问题列表
容易
- 0035. 搜索插入位置 Search Insert Position
- 0069. x 的平方根 Sqrt(x)
- 0278. 第一个错误的版本
- 0349. 两个数组的交集 Intersection of Two Arrays
- 0374. 猜数字大小 Guess Number Higher or Lower
- 0704. 二分查找 Binary Search
- 0744. 寻找比目标字母大的最小字母 Find Smallest Letter Greater Than Target
TODO:
中等
- 0034. 在排序数组中查找元素的第一个和最后一个位置 Find First and Last Position of Element in Sorted Array
- 0074. 搜索二维矩阵 Search a 2D Matrix
- 0153. 寻找旋转排序数组中的最小值 Find Minimum in Rotated Sorted Array
- 0162. 寻找峰值 Find Peak Element
- 0167. 两数之和 II - 输入有序数组 Two Sum II - Input Array Is Sorted
- 0240. Search a 2D Matrix II Search a 2D Matrix II
- 0287. 寻找重复数 Find the Duplicate Number
- 0532. 数组中的数对 K-diff Pairs in an Array
- 0852. 山脉数组的峰顶索引 Peak Index in a Mountain Array
- 1498. 满足条件的子序列数目 Number of Subsequences That Satisfy the Given Sum Condition
TODO:
- 29. 两数相除
- 33. 搜索旋转排序数组 Search in Rotated Sorted Array
- 378. 有序矩阵中第K小的元素
- 436. 寻找右区间
- 454. 四数相加 II
- 792. 匹配子序列的单词数
- 1004. Max Consecutive Ones III
- 2817. Minimum Absolute Difference Between Elements With Constraint
困难
TODO:
排序
简单
中等
- 0056. 合并区间 Merge Intervals
- 0075. 颜色分类 Sort Colors
- 1498. 满足条件的子序列数目 Number of Subsequences That Satisfy the Given Sum Condition
TODO:
困难
链表相关的问题列表
容易
- 0234. 回文链表 Palindrome Linked List
- 0021.合并两个有序链表 Merge Two Sorted Lists
- 0083. 删除排序链表中的重复元素 Remove Duplicates from Sorted List
TODO:
中等
- 0002. 两数相加 Add Two Numbers
- 0082. 删除排序链表中的重复元素 II Remove Duplicates from Sorted List II
- 0707. 设计链表 Design Linked List
TODO:
困难
TODO:
参考
链表排序 Sorting
链表双指针 Two Pointers
队列 Queue
单调队列 Monotonic queue
栈相关的问题列表
容易
TODO:
中等
- 0071. 简化路径 Simplify Path
- 0150. 逆波兰表达式求值 Evaluate Reverse Polish Notation
- 0155. 最小栈 Min Stack
- 0394. 字符串解码 Decode String
TODO:
- 636. 函数的独占时间
- 739. 每日温度
- 856. 括号的分数
- 921. 使括号有效的最少添加
- 946. 验证栈序列
- 1003. 检查替换后的词是否有效
- 1190. 反转每对括号间的子串
- 1209. 删除字符串中的所有相邻重复项
- 面试题 16.26. 计算器
困难
单调栈 Monotonic
简单
中等
困难
优先级队列 Priority Queue
简单
中等
困难
哈稀表相关的问题列表
容易
- 0001. 两数之和 Two Sum
- 0013. 罗马数字转整数 Roman to Integer
- 0217. 存在重复元素 Contains Duplicate
- 0219. 存在重复元素II Contains Duplicate II
- 0349. 两个数组的交集 Intersection of Two Arrays
- 0350. 两个数组的交集 II Intersection of Two Arrays II
TODO:
- 169. Majority Element
- 202.快乐数
- 204. 计数质数
- 205. 同构字符串
- 242. Valid Anagram
- 290. 单词规律
- 387. 字符串中的第一个唯一字符
- 594. 最长和谐子序列
- 599. 两个列表的最小索引总和
- 645. 错误的集合
- 720. 词典中最长的单词
- 884. 两句话中的不常见单词
- 970. 强整数
- 1207.独一无二的出现次数
- 2006. Count Number of Pairs With Absolute Difference K
- 2085. Count Common Words With One Occurrence
- 2215. Find the Difference of Two Arrays
- 2248. Intersection of Multiple Arrays
- 2357. Make Array Zero by Subtracting Equal Amounts
中等
- 0012. 整数转罗马数字 Integer to Roman
- 0167. 两数之和 II - 输入有序数组 Two Sum II - Input Array Is Sorted
- 0532.数组中的数对 K-diff Pairs in an Array
TODO:
- 3.无重复字符的最长子串
- 49. Group Anagrams
- 215. 数组中的第K个最大元素
- 347. 前 K 个高频元素
- 380. 常数时间插入、删除和获取随机元素
- 451. 根据字符出现频率排序
- 648. 单词替换
- 692. 前K个高频单词
- 718. 最长重复子数组
- 2364. Count Number of Bad Pairs
- 2442. Count Number of Distinct Integers After Reverse Operations
字符串相关的问题列表
容易
TODO:
- 13.罗马数字转整数
- 14. 最长公共前缀
- 67. 二进制求和
- 434. 字符串中的单词数
- 819. 最常见的单词
- 859. 亲密字符串
- 686. 重复叠加字符串匹配
- 680. 验证回文字符串 Ⅱ
- 944. Delete Columns to Make Sorted
- 1869. Longer Contiguous Segments of Ones than Zeros
中级
TODO:
- 0006. Z 字形变换
- 0008. 字符串转换整数(atoi)
- 0017.电话号码的字母组合
- 0165.比较版本号
- 2414. Length of the Longest Alphabetical Continuous Substring
字符串匹配 String Matching
字典树 Trie
树相关的问题列表
容易
- 28.对称的二叉树
- 101. 对称二叉树
- 104.二叉树的最大深度
- 111.二叉树的最小深度
- 144. 二叉树的前序遍历
- 530. Minimum Absolute Difference in BST
- 面试题 04.02.最小高度树
中等
- 94.二叉树的中序遍历
- 98. 验证二叉搜索树
- 102. 二叉树的层序遍历
- 144.二叉树的前序遍历
- 145.二叉树的后序遍历
- 199. 二叉树的右视图
- 230.二叉搜索树中第K小的元素
- 297.序列化二叉树
- LCR 124. 推理二叉树, 重建二叉树
- LCR 143. 树的子结构判断
- LCR 149. 彩灯装饰记录 I
- LCR 150. 彩灯装饰记录 II
- LCR 151. 彩灯装饰记录 III
- LCR 153. 二叉树中和为目标值的路径
- LRC 174.二叉搜索树的第k大节点
- 面试题33.二叉搜索树的后序遍历序列
二叉树的遍历 Traversal
二叉树的还原 Restore
二叉搜索树 Binary Search Tree
二叉索引树 Binary Indexed Tree
二叉索引树又称为树状数组.
线段树 Segment Tree
并查集 Union-Find Data Structure
图相关的问题列表
TODO(Shaohua): Update problems list
容易
中等
TODO:
最小生成树 Minimum Spanning Tree
递归相关的问题列表 Recursion
简单
中等
困难
位运算相关的问题列表
简单
- 0067. 二进制求和 Add Binary
- 0136. 只出现一次的数字 Single Number
- 0137. 只出现一次的数字II Single Number II
- 0191. 位1的个数 Number of 1 Bits
- 0231. 2的幂 Power of Two
- 0326. 3的幂 Power of Three
- 0338. 比特位计数 Counting Bits
- 0342. 4的幂 Power of Four
TODO:
- 190. Reverse Bits
- 461. Hamming Distance
- 693. Binary Number with Alternating Bits
- 762. Prime Number of Set Bits in Binary Representation
- 2859. Sum of Values at Indices With K Set Bits
- 2917. Find the K-or of an Array
中等
TODO:
困难
数学问题的问题列表
简单
TODO:
中等
TODO:
回溯法相关的问题列表 Backtracking
简单
TODO:
中等
- 0039.组合总和 Combination Sum
- 0040. 组合总和 II Combination Sum II
- 0046. 全排列 Permutations
- 0047. 全排列 II Permutations II
- 0078. 子集 Subsets
- 0090. 子集 II Subsets II
TODO:
困难
分治法相关的问题列表 Divide and Conquer
简单
中等
困难
深度优先搜索 Depth First Search, DFS
简单
中等
困难
广度优先搜索 Bridth First Search, BFS
简单
中等
困难
动态规划相关的问题列表 Dynamic Programming
容易
TODO:
- 53.最大子序和
- 70. 爬楼梯
- 121. 买卖股票的最佳时机
- 122. 买卖股票的最佳时机 II
- 123. 买卖股票的最佳时机 III
- 188. 买卖股票的最佳时机 IV
- 198. 打家劫舍
- 213. 打家劫舍 II
- 303. 区域和检索
- 309. 最佳买卖股票时机含冷冻期
- 714. 买卖股票的最佳时机含手续费
- 746. 使用最小花费爬楼梯
- 1025. 除数博弈
- 面试题 08.01. 三步问题
中等
TODO:
- 5.最长回文子串
- 53. 最大子数组和
- 63. 不同路径 II
- 64. 最小路径和
- 72. 编辑距离
- 91. 解码方法
- 93. 复原 IP 地址
- 120. 三角形最小路径和
- 139. 单词拆分
- 140. 单词拆分 II
- 152. 乘积最大子序列
- 221. 最大正方形
- 263. 丑数
- 264. 丑数 II
- 300. 最长上升子序列
- 413. 等差数列划分
- 516. 最长回文子序列
- 518. 零钱兑换 II
- 583. 两个字符串的删除操作
- 638. 大礼包 未做
- 647. 回文子串
- 712. 两个字符串的最小ASCII删除和
- 877. 石子游戏
- 931. 下降路径最小和
- 1143. 最长公共子序列
- 1277. 统计全为 1 的正方形子矩阵
- 5454. 统计全 1 子矩形
困难
TODO:
贪心算法相关的问题列表
容易
中等
leetcode 问题分类
这一节, 分别基于问题所属的标签, 和问题编号, 列出问题, 方便索引.
目前采用的刷题顺序:
- 数学
- 比特位操作
- 双指针
- 数组
- 链表
- 哈稀表
- 字符串
- 栈
- 队列
- 树
- 回溯
- 贪心
- 动态规划
- 图
困难程度
难度 | 知识点 |
---|---|
入门 | 数组, 字符串, 链表, 排序 |
简单 | 栈, 队列, 哈希表, 双指针 |
中等 | 二叉树, 堆, 单调栈, 滑动窗口, 二分, 位运算 |
困难 | DP, DFS, BFS, 回溯, 贪心, 并查集, 前缀树 |
问题 0001-0100
0001. 两数之和 Two Sum
方法1, Brute Force
这个方法比较直接, 就是遍历数组, 并遍历后面的每个元素, 求两者之和是否与 target
相等.
因为有两层遍历, 这个方法的时间复杂度是 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
的值, 如果找到, 就可以返回了.
这个方法的时间复杂度是 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
这是一个数学问题. 它考察的知识有这几个方面:
- 如何从十进制的整数中提取出各个位的值
- 如何从各个位的值重新组装一个整数
- 如何处理整数边界溢出的问题
接下来我们分别来说说.
从十进制的整数中提取出各个位的值
要使用 除10
的操作, 进行十进制的右移.
#![allow(unused)] fn main() { let mut x = 1234; while x != 0 { println!("unit value: {}", x % 10); x /= 10; } }
看图:
重新组装整数
如何从各个位的值重新组装一个整数? 使用相反的操作, 乘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}"); }
看图:
整数溢出
如何处理整数边界溢出的问题? 在组装新的整数时, 可以让当前的值 *10
后与 i32::MAX .. i32::MIN
进行比较, 看在不在这个范围内.
number > i32::MAX / 10 || number < i32::MIN / 10
要注意的一点是, 不能使用 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 } }
分段映射
单独对每个进位做映射, 很简单的实现, 易懂.
#![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 } }
模式匹配
#![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)
, 性能也是最差的.
#![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)
.
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;
}
};
哈稀表
这个方法的性能并不好, 但也算是解决问题的一个思路. 具体做法就是:
- 使用两个字典分别存储大于0, 以及小于0的元素; 同时统计0出现的次数
- 首先如果0存在, 尝试用它作为正负数的分隔, 其组合情况如下:
(负数, 0, 正数)
(0, 0, 0)
- 现在考虑非0组合, 可能的组合情况如下:
(负数, 负数, 正数)
(负数, 正数, 正数)
- 将所有的组合都添加到一个集合中, 这样就可以用它来去掉重复的组合
这种思路, 也同样可以用于解决 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, 快慢型双指针法
快慢型双指针法的详细说明, 可以参考这里.
#![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
这个问题适合用二分查找法.
基本的二分查找法
分两步来解这个问题:
- 使用二分查找法确定
target
存在于数组中, 如果不存在就直接返回; 如果存在, 就得到了它的索引值middle
- 然后使用线性查找法, 分别从
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 } }
使用 slice::binary_search
这个是对上述方法的简化, 使用标准库中自带的二分查找算法:
#![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
这是一个排序题.
排序
因为给定的区间是无序的, 我们先以每个区间的起始点来对它进行排序, 之后再统计.
步骤如下:
- 对
intervals
所有区间的起始点进行排序,intervals.sort_by_key(|interval| interval[0])
- 创建一个动态数组
ans
, 用来存储不重叠的区间 - 遍历
intervals
中的所有数对, 然后合并有重叠的区间, 并将不重叠的区间存储到ans
数组中- 如果有重叠, 只需要更新区间的终点值即可
- 如果没有重叠, 则需要把之间的区间存到
ans
数组, 并同时更新起点和重点
- 遍历
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"
相加的过程:
代码写得就比较自然了, 就按上图描述的:
先构造出两个字符串的迭代器, 用于逆序遍历字符串, 这里为了方面, 我们直接用 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)
.
二分查找法
考虑题目中的条件:
- 每行数据是递增的
- 每列数据也是递增的
根据上面的条件, 我们的解决思路是:
- 先使用二分查找法遍列第一列, 找到
target
可能位于哪一行 - 然后再利用二分查找法遍历该行, 确定
target
是否存在于当前行
但要注意一下细节:
- 在查找行时, 我们使用
while top < bottom
的条件判断, 该循环终止的条件是left == right
- 而且计算 middle 值时, 使用的是
let middle = left + (right - left + 1) / 2
, 这样的话, 中间值会偏下一位, 但因为之后的条件,bottom = middle - 1
, 这样才不会进行死循环 - 在查找行数据时, 我们使用
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)
快慢型双指针
典型的快慢型双指针问题.
其中, 快指针用于遍历数组; 慢指针用于移除重复元素后, 指向数组的最高位.
#![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 } }
优先双指针
这里的优化仅仅是简化了思路, 但是性能并不一定更好.
#![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
靠拢型双指针
这是一个典型的双指针问题, 相关介绍可以看这里.
#![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 } }
利用回文的特性
回文的特性就是, 把它反转过来, 会得到一样的结果.
我们只需要反转字符串, 并与原字符串对比一下就能确定, 它是不是回文.
#![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
.
我们可以利用这个特性, 遍历数组中的每一项, 然后计算异或值, 最后的结果就是那个单值.
这个思路, 可以用于快速消除数组中出现偶数次的元素.
代码也非常简单:
#![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次; 求得的余数, 就是落单的数值在该比特位的比特值
- 当遍历完整数的所有比特位后, 就可以计算出落单整数的所有比特位的信息, 也就可以组装出了它的具体数值
基本的操作过程如下图所示:
要注意的是, 这个解决方法, 可以解决数组中有奇数个重复的整数, 也可以解决有偶数个重复的整数. 也就是说, 它可以用来解 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
这个是二分查找法的变体.
先分析题目中的条件:
- 数组中没有重复的元素
- 数组最初是升序排序的
- 数组中的元素被右移了
k
次
基于以上条件, 我们可以推出, 数组中元素可能的布局只有三种情况:
(1, 2, 3)
(3, 1, 2)
(2, 3, 1)
以上的布局决定了二分查找法中的 middle
元素所在的趋势. 大概如下图所示:
有了这样的分析, 就可以编写二分查找算法了:
#![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
位于峰值的右侧
我们编写二分查找法时, 就可以根据这些情况来处理:
#![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
三次反转法
操作过程如下:
- 将
arr[k..n]
进行反转 - 将
arr[0..k]
进行反转 - 将
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(); } }
使用时时数组
操作过程如下:
- 创建辅助数组
- 将
arr[(len - k)..]
复制到辅助数组 - 将
arr[..(len - k)]
复制到辅助数组 - 将辅助数组中的内容与目标数组交换, 通过
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
这个题目考察位操作的.
先考虑比特位操作的函数表:
A | B | A OR B | A AND B | A XOR B | NOT A |
---|---|---|---|---|---|
1 | 1 | 1 | 1 | 0 | 0 |
1 | 0 | 1 | 0 | 1 | 0 |
0 | 1 | 1 | 0 | 1 | 1 |
0 | 0 | 0 | 0 | 0 | 1 |
其次是如何遍历 u32 的所有比特位? 是的, 用右移 (shift right), 依次将每一个比特位右移到最低位,
然后结合上面的逻辑与操作(bitwise AND
), 就可以取出这个比特位的值, 是 0 或者 1.
下图展示的是 n=11 (即 0b1011
) 时的右移操作:
想明白了上面的过程, 代码就比较容易了.
#![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, 查找当前元素是否在后面出现过.
这个方法的时间复杂度为 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 做排序, 然后应该只需要遍历它一次, 每个元素与相邻的元素比较是否相等, 就可以判定 是否包含重复的元素.
这个方法的时间复杂度为 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)
), 我们可以用它来存储元素, 加快查找.
先遍历整个数组, 查找集合中是否存在该元素, 如果存在就返回, 如果不存在就把它插入到集合中.
这个方法的时间复杂度为 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做一下修改:
- 遍历整个数组, 将里面的所有元素都插入到集合中
- 比较集合中元素的个数是否与数组中元素的个数相同, 如果不同, 那就说明有重复元素
这个方法是利用了 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
这个方法比较直接, 就是遍历数组, 并遍历后面的每个元素, 判断它们是否重复.
因为有两层遍历, 这个方法的时间复杂度是 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, 哈稀表
同样是需要遍历整个数组, 我们可以使用哈稀表缓存一下访问过的元素, 以加快查找元素的时间. 这个哈稀表用于记录元素值到它在数组中的索引值之间的关系.
这个方法的时间复杂度是 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
- 其它情况, 该位置的值是
整个数组的积 / 当前元素的值
- 如果为0, 整个数组中0的个数为1, 说明其它元素都不为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
- 从左到右遍历原数组, 并累积计算元素的乘积, 将乘积保存到目标数组中的相同位置
- 从右到左遍历原数组, 并累积计算元素的乘积, 目标数组中的相同位置的元素与该乘积结果相乘, 就是该元素的最终值
代码实现如下:
#![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
, 有三种情况:
- 创建
row_index
和col_index
, 用于定位二维数组中的元素; 并在开始时将它定位到数组右上角 - 当前元素等于
target
, 就直接返回 - 当前元素比
target
大, 那我们就将col_index -= 1
, 向左侧继续查找 - 当前元素比
target
小, 那我们就将row_index += 1
, 去下一行继续查找 - 直找我们遍历到二维数组的左下角终止循环, 此时
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
- 先对数组进行排序 (这里已经不符合题目要求了)
- 然后遍历数组, 找到左右相邻元素之间有重复的
#![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)
使用负数来标记该整数已经出现过
这个方法也会修改数组的值, 其核心思想是利用整数的符号位来标记整数是否存在. 其步骤如下:
- 遍历数组, 计数当前元素
m
的绝对值 - 以该绝对值作为元素的索引, 找到数组中对应的元素
k
- 如果元素
k
本身是一个正数, 就把它转成对应的负数 - 如果元素
k
是一个负数, 说明之前就有相同的整数值m
存在过, 就返回它
下面以 [1, 2, 3, 3, 4]
数组为例展示整个过程:
当遍历到 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的次幂.
这个方法也是通用的, 可以用来计算任意正整数的次幂.
#![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的次幂.
如果不好理解的话, 可以看下图:
要晓得的是, 这个方法只对质数的次幂有效.
关于如何计算最大次幂, 代码里的辅助函数有说明, 要注意的一点是整数相乘的溢出问题, 我们是利用了 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
可以看下面的图:
时间复杂度是 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) 问题.
如果整数 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
这个也比较符合并行双指针的使用场景, 遍历两个数组或链表, 用它能很快地计算集合的交集和并集.
在使用双指针之前, 要先对数组进行排序, 让重复的元素挨在一起.
这种方法也合适输出结果包含重复元素的问题, 只要不去重即可.
#![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
了. 要注意的点有两个:
- 交集, 元素要在
nums1
和nums2
中都存在 - 输出的结果不允许有重复的元素
这种方法只适合输出结果中不包含重复元素的问题.
#![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 } }
二分查找法 Binary Search
上面提到了交集的两个特点:
- 交集, 元素要在
nums1
和nums2
中都存在 - 输出的结果不允许有重复的元素
除了使用 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 中 有好几个解法.
但是如果允许存在重复元素的话, 那可用的算法就少一些了.
并行双指针
这个方法依然有效, 只需要不跳过重复元素即可.
关于并行双指针的详细说明可以看这里.
#![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
可以用来处理包含有重复元素的集合.
#![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]
为例:
代码实现
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
滑动窗口
这是一个简单的滑动窗口的问题.
遍历数组, 计算出每个窗口的大小, 然后找出最大值即可.
要注意的点是:
- 遍历完数组后, 要检查最后一个窗口是不是最大值
#![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 来存放的有序数对, 并去掉重复的.
这种方法可以支持无序数组.
#![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
这个方法的效率是最高的, 也最节省内存.
解决问题之前依然要先给数组排序.
这个题目中, 双指针的命中条件是 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();
暴力法
利用题目中的要求, 按序依次跳过数组中的一个元素, 并判断它组成的新数组是不是回文.
#![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 元素将数组分成左右两部分, 分别叫
prefix array
和suffix 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
大的第一个元素.
二分查找法
思路是排除法, 因为左侧的元素较小, 优先排除左侧部分. 步骤如下:
- 创建两个指针, 分别指向数组的左右两侧
- 开始二分查找, 计算中间节点 middle 的值, 并判断:
- 如果
letters[middle] >= target
, 说明[left..middle]
区间的字符较小, 将 left 指针向右移, 令left = middle + 1
- 否则, 说明
[middle..right]
的字符都是比target
大的, 将 right 指针左移, 令right = middle
- 如果
- 当
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
的右侧
但我们对它进行了一下简化, 步骤是:
- 创建两个指针, 其中 left 指向数组的最左侧, right 指向数组最右侧
- 开始二分查找法的循环, 计算中间节点 middle, 并确定 middle 与峰值的关系:
- 如果
arr[middle] < arr[middle + 1]
, 说明峰值位于 middle 的右侧, 此时向右移动 left 指针, 令left = middle + 1
- 否则说明峰值位于 middle 处或者在其左侧, 此时向左移右 right 指针, 令
right = middle
- 如果
- 当
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
要注意的是:
- 如果已经遍历完
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
的个数,
然后计算其中最大的那个组合.
具体步骤是:
- 创建
count_zeros
数组, 用于存放从前向后字符0
出现的次数之和 - 从前向后遍历字符串, 统计出字符
0
出现的次数之和, 并存入count_zeros
数组 - 创建
count_ones
数组, 用于存放从后向前字符1
出现的次数之和 - 从后向前遍历字符串, 统计出字符
1
出现的次数之和, 并存入count_ones
数组 - 将
count_ones
数组反转, 方便后面的计算 - 遍历计数数组
(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
就是除数- 一次兑换交易, 从空瓶子换成满瓶水, 就是一个整数的除法操作
- 既然是整数除法, 那就有可能还有余数
代码里有更详细的注释:
#![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, 就可以最终解决问题了.
如果还不好理解, 就看一下下图:
基于以上的解释, 就可以写代码了:
#![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
计数步骤如下:
- 创建字典, 为了找到相同生存人数的最小年份, 我们使用 BTreeMap 来保证年份的有序.
- 遍历
logs
中的所有记录, 并遍历每条记录的出生到去世间的所有年份, 将本年度加入到字典中 - 遍历有序字典, 找到有最大生存人数的那一年
#![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)
, 其中 n
是 logs
的年份范围.
计数数组
因为年份跨度比较小, 只有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)
, 其中 n
是 logs
的年份范围.
前缀和
这个有些不好考虑, 在构造前缀和数组之前, 我们先构造一个辅助数组. 整个解决步骤如下:
- 创建
alive
辅助数组 - 遍历
logs
数组- 当一个人出生时, 将 alive 中出生所在年份计数加1,
alive[start_year] += 1
- 当一个人去世时, 将 alive 中去世所在年份计数减1,
alive[end_year] -= 1
- 当一个人出生时, 将 alive 中出生所在年份计数加1,
- 创建
prefix_sum
前缀和数组, 通过遍历alive
数组 - 最后遍历
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)
, 其中 n
是 logs
的年份范围.
1893. 检查是否区域内所有整数都被覆盖 Check if All the Integers in a Range Are Covered
这个问题有两个思路可以处理.
集合 Set
使用集合来存储区间上的每个点.
步骤如下:
- 创建集合
- 遍历
ranges
数组, 将每个范围上的所有点位都存储到集合中 - 遍历
[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
这个方法用于计算区间重叠很方便.
其步骤如下:
- 先对
ranges
数组进行排序, 依照范围的起始点 - 构造合并区间
intervals
- 初始化区间值 start = 0, end = 0
- 遍历 ranges, 并判断当前区间是否能跟区间值
[start..=end]
拼接在一起, 判定条件是range[0] <= end + 1
- 如果可以, 就只需要移动区间的终点值,
end = end.max(range[1])
- 如果不行, 就先将当前区间
[start..=end]
加入到intervals
, 然后更新[start..=end]
区间
- 查找
[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
, 将十进制的整数右移一位即可.
最终的代码如下:
#![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..=n]
的前缀和数组 - 遍历前缀和数组, 如果左侧部分等于右侧部分, 就返回,
prefix * 2 == total_sum + i as i32
- 否则没有找到, 就返回 -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)
.
双指针法
步骤如下:
- 构造两个指针, 分别代表左侧之和与右侧之和
- 左侧之和初始化为0
- 右侧之和被初始化为
n * (n + 1) /2
- 遍历
1..=n
, 然后判断左侧之和与右侧之和的关系- 如果
left_sum + i == right_sum
, 说明找到了该位置, 直接返回 - 否则更新左侧之和与右侧之和
- 如果
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
处理思路如下:
- 计算
left_sum
, 从左到右遍历原数组, 并计算前缀和,left_sum[i + 1] = left_sum[i] + nums[i]
- 计算
right_sum
从左到右遍历原数组, 并计算前缀和,right_sum[i - 1] = right_sum[i] + nums[i]
- 计算遍历两个数组, 并计算
(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, 我们用集合来统计每辆车占用的点位, 最后计算集合中点的个数就行.
操作步骤如下:
- 创建统计用的集合
- 遍历所有车辆, 将每个车辆占用的点位区间上的所有点, 都加入到集合中
- 集合的长度就是所有的点位数
#![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 的解法.
因为给定的区间是无序的, 我们先以每个区间的起始点来对它进行排序, 之后再统计.
步骤如下:
- 对所有车辆所占用的区间的起始点进行排序
- 创建一个动态数组
intervals
, 用来存储不重叠的区间 - 遍历
nums
中的所有数对, 然后合并有重叠的区间, 并将不重叠的区间存储到intervals
数组中- 如果有重叠, 只需要更新区间的终点值即可
- 如果没有重叠, 则需要把之间的区间存到
intervals
数组, 并同时更新起点和重点
- 遍历
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
这是一个经典的前缀和数组问题.
解题思路如下:
- 先遍历
nums
数组, 构造出前缀和数组positions
- 遍历
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)
.
2024年E卷100分
- 流浪地球
- 斗地主之顺子 - 栈
- 数大雁
- 最大利润/贪心的商人 - 贪心算法
- boss的收入 - 递归
- 猜字谜 - 字符串
- 猜数字
- 最大报酬 - 背包问题
- 分糖果 - 位运算
- 日志采集系统 - 动态规划
- 最左侧冗余覆盖子串 - 滑动窗口
- 增强的strstr - 字符串
- 报文响应时间 - 字符串
- 连续字母长度 - 字符串
- 最长连续子序列 - 滑动窗口
- 计算面积/绘图机器
- 敏感字段加密 - 字符串
- 出租车计费/靠谱的车 - 数学
- 分苹果 - 位运算
- 字符串变换最小字符串 - 贪心算法
- 字符串分割转换 - 字符串
- 简单的自动曝光/平均像素值
- 计算三叉搜索树的高度 - 树
- 补种未成活胡杨 - 滑动窗口
- 最小的调整次数/特异性双端队列
- 查找充电设备组合 - 动态规划
- 智能成绩表 - 排序
- 虚拟理财游戏 - 栈
- 手机App防沉迷系统 - 排序
- 构成正方形的数量
- 单词接龙 - 字符串
- 跳房子I
- 第k个排列 - 排列组合
- 喊7的次数重排
- 英文输入法 - 字符串
- 高矮个子排队 - 滑动窗口
- 考勤信息
- 找终点
- 数组拼接 - 字符串
- 整数对最小和
- 环中最长子串/字符成环找偶数O - 字符串
- 找数字/找等值元素
- 光伏场地建设规划 - 前缀和
- We Are A Team - 并查集
- 生成哈夫曼树 - 树
- 内存资源分配 - 贪心
- 矩形相交的面积 - 数学
- 水仙花数I - 数学
- 不等式是否满足约束并输出最大差
- 字符统计及重排 - 字符串
- VLAN资源池
流浪地球
题目描述
流浪地球计划在赤道上均匀部署了N个转向发动机, 按位置顺序编号为0~N-1.
- 初始状态下所有的发动机都是未启动状态
- 发动机启动的方式分为"手动启动"和"关联启动"两种方式
- 如果在时刻1一个发动机被启动, 下一个时刻2与之相邻的两个发动机就会被"关联启动"
- 如果准备启动某个发动机时, 它已经被启动了, 则什么都不用做
- 发动机0与发动机N-1是相邻的
地球联合政府准备挑选某些发动机在某些时刻进行"手动启动", 当然最终所有的发动机都会被启动.
哪些发动机最晚被启动呢?
输入描述
- 第一行两个数字N和E, 中间有空格
- N代表部署发动机的总个数, E代表计划手动启动的发动机总个数
1 < N <= 1000
,1 <= E <= 1000
,E <= N
- 接下来共E行, 每行都是两个数字T和P, 中间有空格
- T代表发动机的手动启动时刻, P代表此发动机的位置编号
0 <= T <= N
,0 <= P < N
输出描述
- 第一行一个数字N, 以回车结束
- N代表最后被启动的发动机个数
- 第二行N个数字, 中间有空格, 以回车结束
- 每个数字代表发动机的位置编号, 从小到大排序
示例1
输入:
8 2
0 2
0 6
输出:
2
0 4
示例2
输入:
8 2
0 0
1 7
输出:
1
4
题解
对于离散型的问题, 我们可以使用快照的方式, 下个快照里的状态是基于当前状态的变化.
Python
def main():
# 读取输入
parts = input().split()
assert len(parts) == 2
num_engines = int(parts[0])
num_initial_startup = int(parts[1])
# 引擎初始状态
initials = []
# 哪些引擎是被"手动启动"的
for i in range(num_initial_startup):
parts = input().split()
assert len(parts) == 2
tick = int(parts[0])
position = int(parts[1])
initials.append((tick, position))
# 标记引擎是否点火
engines = [False for i in range(num_engines)]
engines_started = 0
# 记录本轮中点火的引擎
started_this_round = []
# 模拟每个时间点
for tick in range(num_engines):
# 如果所有引擎都已点火, 就终止循环
if engines_started == num_engines:
break
started_this_round.clear()
# 当前时间点中的快照
snapshot = engines[:]
# "关联启动"模式, 启动相邻的引擎
for index in range(num_engines):
# 当前引擎已经被启动
if engines[index]:
#print("CHECK sibling:", index)
previous_index = (num_engines + index - 1) % num_engines
next_index = (index + 1) % num_engines
if not snapshot[previous_index]:
snapshot[previous_index] = True
started_this_round.append(previous_index)
engines_started += 1
#print(" START previous:", previous_index)
if not snapshot[next_index]:
snapshot[next_index] = True
started_this_round.append(next_index)
engines_started += 1
#print(" START next:", next_index)
# 检查"手动启动"的引擎
for (initial_tick, initial_position) in initials:
if initial_tick == tick and not snapshot[initial_position]:
snapshot[initial_position] = True
engines_started += 1
started_this_round.append(initial_position)
#print("START initial:", initial_position)
# 保存快照
engines = snapshot
# 打印结果
print("%d" % len(started_this_round))
print(" ".join(str(pos) for pos in started_this_round))
if __name__ == "__main__":
main()
C++
#include <cassert>
#include <iostream>
#include <tuple>
#include <vector>
int main() {
// 读取输入
int num_engines = 0 ;
int num_initial_startup = 0;
std::cin >> num_engines >> num_initial_startup;
assert(1 <= num_engines && num_engines <= 1000);
assert(1 <= num_initial_startup && num_engines <= 1000);
// 引擎初始状态, (tick, position)
std::vector<std::tuple<int, int>> initials;
// 哪些引擎是被"手动启动"的
for (int i = 0; i < num_initial_startup; ++i) {
int tick = 0;
int pos = 0;
std::cin >> tick >> pos;
initials.emplace_back(tick, pos);
}
// 标记引擎是否点火
std::vector<bool> engines(num_engines, false);
int engines_started = 0;
// 记录本轮中点火的引擎
std::vector<int> started_this_round;
// 模拟每个时间点
// 如果所有引擎都已点火, 就终止循环
for (int tick = 0; engines_started < num_engines; ++tick) {
started_this_round.clear();
// 当前时间点中的快照
std::vector<bool> snapshot = engines;
// "关联启动"模式, 启动相邻的引擎
for (int index = 0; index < num_engines; ++index) {
// 当前引擎已经被启动
if (engines[index]) {
//std::cout << "CHECK sibling: " << index << std::endl;
const int previous_index = (num_engines + index - 1) % num_engines;
const int next_index = (index + 1) % num_engines;
if (!snapshot[previous_index]) {
snapshot[previous_index] = true;
started_this_round.push_back(previous_index);
engines_started += 1;
//std::cout << " START previous: " << previous_index << std::endl;
}
if (!snapshot[next_index]) {
snapshot[next_index] = true;
started_this_round.push_back(next_index);
engines_started += 1;
//std::cout << " START next: " << next_index << std::endl;
}
}
}
// 检查"手动启动"的引擎
for (const auto initial: initials) {
const int initial_tick = std::get<0>(initial);
const int initial_position = std::get<1>(initial);
if (initial_tick == tick && !snapshot[initial_position]) {
snapshot[initial_position] = true;
engines_started += 1;
started_this_round.push_back(initial_position);
}
}
// 保存快照
engines = snapshot;
}
// 打印结果
std::cout << started_this_round.size() << std::endl;
for (int i = 0; i + 1 < started_this_round.size(); ++i) {
std::cout << started_this_round[i] << " ";
}
if (!started_this_round.empty()) {
std::cout << started_this_round[started_this_round.size() - 1] << std::endl;
}
return 0;
}
Rust
use std::io::{stdin, BufRead}; fn main() { // 读取输入 let mut line = String::new(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let mut parts = line.split_ascii_whitespace(); let num_engines: usize = parts.next().unwrap().parse().unwrap(); let num_initial_startup: usize = parts.next().unwrap().parse().unwrap(); assert!((1..=1000).contains(&num_engines)); assert!((1..=1000).contains(&num_initial_startup)); // 引擎初始状态 let mut initials = Vec::with_capacity(num_initial_startup); // 哪些引擎是被"手动启动"的 for _i in 0..num_initial_startup { line.clear(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let mut parts = line.split_ascii_whitespace(); let tick: usize = parts.next().unwrap().parse().unwrap(); let pos: usize = parts.next().unwrap().parse().unwrap(); initials.push((tick, pos)); } // 标记引擎是否点火 let mut engines = vec![false; num_engines]; let mut engines_started = 0; // 记录本轮中点火的引擎 let mut started_this_round = Vec::<usize>::new(); // 模拟每个时间点 for tick in 0..num_engines { // 如果所有引擎都已点火, 就终止循环 if engines_started == num_engines { break; } started_this_round.clear(); // 当前时间点中的快照 let mut snapshot = engines.clone(); // "关联启动"模式, 启动相邻的引擎 for (index, engine_started) in engines.iter().enumerate() { // 当前引擎已经被启动 if *engine_started { println!("CHECK sibling: {index}"); let previous_index = (num_engines + index - 1) % num_engines; let next_index = (index + 1) % num_engines; if !snapshot[previous_index] { snapshot[previous_index] = true; started_this_round.push(previous_index); engines_started += 1; println!(" START previous: {previous_index}"); } if !snapshot[next_index] { snapshot[next_index] = true; started_this_round.push(next_index); engines_started += 1; println!(" START next: {next_index}"); } } } // 检查"手动启动"的引擎 for (initial_tick, initial_position) in &initials { if *initial_tick == tick && !snapshot[*initial_position] { snapshot[*initial_position] = true; engines_started += 1; started_this_round.push(*initial_position); println!("START initial: {initial_position}"); } } // 保存快照 engines.clone_from(&snapshot); } // 打印结果 println!("{}", started_this_round.len()); let s = started_this_round .into_iter() .map(|x| x.to_string()) .collect::<Vec<_>>() .join(" "); println!("{s}"); }
斗地主之顺子
题目描述
在斗地主扑克牌游戏中, 扑克牌由小到大的顺序为: 3,4,5,6,7,8,9,10,J,Q,K,A,2
,
玩家可以出的扑克牌阵型有: 单张, 对子, 顺子, 飞机, 炸弹等.
其中顺子的出牌规则为: 由至少5张由小到大连续递增的扑克牌组成, 且不能包含2.
例如: 3,4,5,6,7
, 3,4,5,6,7,8,9,10,J,Q,K,A
都是有效的顺子; 而 J,Q,K,A,2
, 2,3,4,5,6
, 3,4,5,6
, 3,4,5,6,8
等都不是顺子.
给定一个包含13张牌的数组, 如果有满足出牌规则的顺子, 请输出顺子.
如果存在多个顺子, 请每行输出一个顺子, 且需要按顺子的第一张牌的大小 (必须从小到大) 依次输出.
如果没有满足出牌规则的顺子, 请输出 No
.
输入描述
13张任意顺序的扑克牌, 每张扑克牌数字用空格隔开, 每张扑克牌的数字都是合法的, 并且不包括大小王:
2 9 J 2 3 4 K A 7 9 A 5 6
不需要考虑输入为异常字符的情况
输出描述
组成的顺子, 每张扑克牌数字用空格隔开:
3 4 5 6 7
示例1
输入:
2 9 J 2 3 4 K A 7 9 A 5 6
输出:
3 4 5 6 7
说明: 13张牌中可以组成的顺子只有1组: 3 4 5 6 7
.
示例2
输入:
2 9 J 10 3 4 K A 7 Q A 5 6
输出:
2
3 4 5 6 7
9 10 J Q K A
示例3
输入:
2 9 9 9 3 4 K A 10 Q A 5 6
输出:
No
题解
Python
import sys
def num_to_card(num: int) -> str:
mapping = [
# 忽略无效的值
"", "", "",
# 从3到 A, 2
"3", "4", "5", "6", "7", "8", "9", "10",
"J", "Q", "K", "A", "2"
]
return mapping[num]
def card_to_num(card: str) -> int:
mapping = {
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"10": 10,
"J": 11,
"Q": 12,
"K": 13,
"A": 14,
"2": 15,
}
return mapping[card]
def main():
# 读取所有的牌
cards = [card_to_num(card) for card in input().split()]
cards.sort()
assert len(cards) == 13
flash_list = []
last_flash_card = 15
first_card = 3
while first_card < 10:
if first_card not in cards:
first_card += 1
continue
temp_cards = []
for card in range(first_card, last_flash_card):
if card in cards:
temp_cards.append(card)
elif len(temp_cards) >= 5:
# 保存顺子
for card in temp_cards:
cards.remove(card)
flash_list.append(temp_cards)
temp_cards = []
else:
for card in temp_cards:
cards.remove(card)
temp_cards = []
# 检查最后一组顺子
if len(temp_cards) >= 5:
# 保存顺子
for card in temp_cards:
cards.remove(card)
flash_list.append(temp_cards)
temp_cards = []
# 给顺子排序
# 1. 基于顺子中的第一张牌
# 2. 基于顺子的长度
flash_list.sort(key = lambda flash: (flash[0], len(flash)))
# 打印结果
# 将数字转换成牌
if flash_list:
print(len(flash_list))
for flash in flash_list:
print(" ".join(num_to_card(num) for num in flash))
else:
print("No")
if __name__ == "__main__":
main()
Rust
use std::io::{stdin, BufRead}; use std::mem; fn num_to_card(num: i32) -> &'static str { match num { // 从3到 A, 2 // 忽略无效的值 3 => "3", 4 => "4", 5 => "5", 6 => "6", 7 => "7", 8 => "8", 9 => "9", 10 => "10", 11 => "J", 12 => "Q", 13 => "K", 14 => "A", 15 => "2", _ => panic!("Invalid card num"), } } // 将牌转换成整数 fn card_to_num(card: &str) -> i32 { match card { "3" => 3, "4" => 4, "5" => 5, "6" => 6, "7" => 7, "8" => 8, "9" => 9, "10" => 10, "J" => 11, "Q" => 12, "K" => 13, "A" => 14, "2" => 15, _ => panic!("Invalid card"), } } fn remove_slice<T: PartialEq>(list1: &mut Vec<T>, list2: &[T]) -> usize { let mut count = 0; for item in list2 { if let Some(pos) = list1.iter().position(|x| x == item) { list1.remove(pos); count += 1; } } count } fn main() { // 读取所有的牌 let mut line = String::new(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let mut cards: Vec<i32> = line.split_ascii_whitespace().map(card_to_num).collect(); cards.sort_unstable(); assert_eq!(cards.len(), 13); let mut flash_list = Vec::new(); let last_flash_card = 15; let mut first_card = 3; // 找出所有的顺子 while first_card < 10 { // 顺子中的第一张牌. if !cards.contains(&first_card) { first_card += 1; continue; } let mut temp_cards = Vec::new(); for card in first_card..last_flash_card { if cards.contains(&card) { temp_cards.push(card); } else if temp_cards.len() >= 5 { // 有效顺子, 保存顺子 remove_slice(&mut cards, &temp_cards); let mut flash = Vec::new(); mem::swap(&mut flash, &mut temp_cards); flash_list.push(flash); } else { // 无效顺子 remove_slice(&mut cards, &temp_cards); temp_cards.clear(); } } // 检查最后一组顺子 if temp_cards.len() >= 5 { // 保存顺子 remove_slice(&mut cards, &temp_cards); flash_list.push(temp_cards); } } // 给顺子排序 // 1. 基于顺子中的第一张牌 // 2. 基于顺子的长度 flash_list.sort_by_key(|flash| (flash[0], flash.len())); // 打印结果 // 将数字转换成牌 if !flash_list.is_empty() { println!("{}", flash_list.len()); for flash in flash_list { let s = flash .into_iter() .map(num_to_card) .collect::<Vec<_>>() .join(" "); println!("{s}"); } } else { println!("No"); } }
数大雁
题目描述
一群大雁往南飞, 给定一个字符串记录地面上的游客听到的大雁叫声, 请给出叫声最少由几只大雁发出.
具体的:
- 大雁发出的完整叫声为"quack", 因为有多只大雁同一时间嘎嘎作响, 所以字符串中可能会混合多个"quack"
- 大雁会依次完整发出"quack", 即字符串中 'q', 'u', 'a', 'c', 'k' 这5个字母按顺序完整存在才能计数为一只大雁; 如果不完整或者没有按顺序则不予计数
- 如果字符串不是由 'q', 'u', 'a', 'c', 'k' 字符组合而成, 或者没有找到一只大雁, 请返回
-1
输入描述
一个字符串, 包含大雁 quack 的叫声, 1 <= 字符串长度 <= 1000
, 字符串中的字符只有 'q', 'u', 'a', 'c', 'k'.
输出描述
大雁的数量
示例1
输入:
quackquack
输出:
1
示例2
输入:
qaauucqcaa
输出:
-1
示例3
输入:
quacqkuac
输出:
1
示例4
输入:
qququaauqccauqkkcauqqkcauuqkcaaukccakkck
输出:
5
题解
Python
import sys
def solution():
# 读取输入
string = input().strip()
# 记录每个 "quack" 叫声在字符串在的位置, 包括起始和结束
quack_pairs = []
# 记录 "q" 字符在字符串中的位置
q_index = []
# 记录 u/a/c 三个字符在字符串中出现的次数
u_count = 0
a_count = 0
c_count = 0
# 先遍历字符串, 找出所有的 "quack" 字符串
for i in range(len(string)):
char = string[i]
if char == "q":
# 把字符 "q" 所在位置存储起来
q_index.append(i)
elif char == "u":
# 如果 有足够多的字符 "q", 就将字符"u"的计数加1
if len(q_index) > u_count:
u_count += 1
elif char == "a":
# 如果 有足够多的字符 "u", 就将字符"a"的计数加1
if u_count > a_count:
a_count += 1
elif char == "c":
# 如果 有足够多的字符 "a", 就将字符"c"的计数加1
if a_count > c_count:
c_count += 1
elif char == "k":
# 如果有字符 "c", 就说明可以组成一个有效的叫声
if c_count > 0:
# 记录下当前的叫声, 包括起始点和结束点
quack_pairs.append((q_index.pop(), i))
# 同时减去字符计数
u_count -= 1
a_count -= 1
c_count -= 1
else:
# 无效字符
print("Invalid char", char)
return -1
# 没有找到有效的叫声
if len(quack_pairs) == 0:
print("quack_paris is empty")
return -1
# 接下来, 找出重叠的 "quack" 字符串有多少个, 并取它们的最大值
max_quack_count = 1
for i in range(len(quack_pairs)):
# 以当前叫声为起点, 找出所有重叠的叫声
current_count = 1
for j in range(i + 1, len(quack_pairs)):
# 如果有重叠, 计数就加1
if quack_pairs[i][1] >= quack_pairs[j][0]:
current_count += 1
# 更新最大重叠的叫声数
max_quack_count = max(max_quack_count, current_count)
return max_quack_count
def main():
count = solution()
print(count)
if __name__ == "__main__":
main()
最大利润/贪心的商人
TODO
boss的收入
题目描述
一个XX产品行销总公司, 只有一个boss, 其有若干一级分销, 一级分销又有若干二级分销, 每个分销只有唯一的上级分销.
规定, 每个月下级分销需要将自己的总收入 (自己的 + 下级上交的) 每满100元上交15元给自己的上级.
现给出一组分销的关系, 和每个分销的收入, 请找出boss并计算出这个boss的收入.
比如:
- 收入100元, 上交15元
- 收入199元 (99元不够100), 上交15元
- 收入200元, 上交30元
输入:
分销关系和收入: [[分销id 上级分销id 收入], [分销id 上级分销id 收入], [分销id 上级分销id 收入]]
- 分销ID范围:
0…65535
- 收入范围:
0…65535
, 单位元
提示: 输入的数据只存在1个boss, 不存在环路.
输出:
[boss的ID, 总收入]
输入描述
- 第一行输入关系的总数量 N
- 第二行开始, 输入关系信息, 格式:
分销ID 上级分销ID 收入
比如:
5
1 0 100
2 0 199
3 0 200
4 0 200
5 0 200
输出描述
输出:
boss的ID 总收入
比如:
0 120
备注: 给定的输入数据都是合法的, 不存在环路, 重复的.
示例一
输入:
5
1 0 100
2 0 199
3 0 200
4 0 200
5 0 200
输出:
0 120
题解
可以使用递归的思想, 先找出 Boss 的ID, 然后计算他的收入; 计算其收入时, 又要先计算他手下人的收入.
要注意的一个问题点是, 收入不足100时不被计算.
Python
import sys
def solution():
salary_tree = dict()
hierachy_tree = dict()
# 读取输入
first_line = input()
for line in sys.stdin.readlines():
parts = line.split()
current_id = int(parts[0])
parent_id = int(parts[1])
salary = int(parts[2])
salary_tree[current_id] = (parent_id, salary)
sibling = hierachy_tree.get(parent_id)
if sibling is None:
sibling = []
hierachy_tree[parent_id] = sibling
sibling.append(current_id)
# 先得到 boss 的 ID
boss_id = current_id
while boss_id in salary_tree:
boss_id = salary_tree[boss_id][0]
print("boss id:", boss_id)
# 递归计算一个 ID 的收入
def get_salary_recursive(parent_id):
if parent_id in hierachy_tree:
children = hierachy_tree[parent_id]
total_salary = 0
for i in range(len(children)):
child = children[i]
salary = get_salary_recursive(child)
total_salary += (salary // 100) * 15
return total_salary
else:
# 得到当前用户的收入
return salary_tree[parent_id][1]
boss_salary = get_salary_recursive(boss_id)
print(boss_salary)
def main():
solution()
if __name__ == "__main__":
main()
C++
#include <cassert>
#include <iostream>
#include <unordered_map>
#include <vector>
// 递归计算一个 ID 的收入
int get_salary_recursive(int parent_id,
std::unordered_map<int, std::vector<int>>& hierachy_tree,
std::unordered_map<int, std::pair<int, int>>& salary_tree
) {
if (hierachy_tree.find(parent_id) != hierachy_tree.cend()) {
const std::vector<int> children = hierachy_tree[parent_id];
int total_salary = 0;
for (int child_id : children) {
const int salary = get_salary_recursive(child_id, hierachy_tree, salary_tree);
total_salary += (salary / 100) * 15;
}
return total_salary;
} else {
// 得到当前用户的收入
return salary_tree[parent_id].second;
}
}
void solution() {
// 用于存储各个ID之间的关系, (parent-id, [children])
std::unordered_map<int, std::vector<int>> hierachy_tree;
// 用于存储各个用户的salary, (user-id, (parent-id, salary))
std::unordered_map<int, std::pair<int, int>> salary_tree;
// 读取输入
int num_persons = 0;
std::cin >> num_persons;
int parent_id = 0;
for (int i = 0; i < num_persons; ++i) {
int current_id = 0;
int salary = 0;
std::cin >> current_id >> parent_id >> salary;
hierachy_tree[parent_id].push_back(current_id);
salary_tree.emplace(current_id, std::make_pair(parent_id, salary));
}
// 先得到 boss 的 ID
int boss_id = parent_id;
for (auto iter = salary_tree.find(boss_id); iter != salary_tree.cend(); iter = salary_tree.find(boss_id)) {
boss_id = iter->second.first;
}
//std::cout << "boss id: " << boss_id << std::endl;
const int boss_salary = get_salary_recursive(boss_id, hierachy_tree, salary_tree);
// 打印结果
std::cout << boss_salary << std::endl;
}
Rust
#![allow(unused)] fn main() { use std::collections::HashMap; use std::io::{stdin, BufRead}; fn solution() { let mut salary_tree = HashMap::<i32, (i32, i32)>::new(); let mut hierachy_tree = HashMap::<i32, Vec<i32>>::new(); // 读取输入 let mut line = String::new(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let num_persons: usize = line.trim().parse().unwrap(); let mut parent_id: i32 = 0; for line in stdin().lock().lines() { let line = line.unwrap(); let mut parts = line.split_ascii_whitespace(); let current_id: i32 = parts.next().unwrap().parse().unwrap(); parent_id = parts.next().unwrap().parse().unwrap(); let salary: i32 = parts.next().unwrap().parse().unwrap(); salary_tree.insert(current_id, (parent_id, salary)); hierachy_tree.entry(parent_id).or_default().push(current_id); } assert_eq!(num_persons, salary_tree.len()); // 先得到 boss 的 ID let mut boss_id = parent_id; while let Some(entry) = salary_tree.get(&boss_id) { boss_id = entry.0; } //println!("boss id: {boss_id}"); // 递归计算一个 ID 的收入 fn get_salary_recursive( parent_id: i32, salary_tree: &mut HashMap<i32, (i32, i32)>, hierachy_tree: &HashMap<i32, Vec<i32>>, ) -> i32 { if let Some(children) = hierachy_tree.get(&parent_id) { let mut total_salary = 0; for &child in children { let salary = get_salary_recursive(child, salary_tree, hierachy_tree); total_salary += (salary / 100) * 15; } total_salary } else { // 得到当前用户的收入 salary_tree[&parent_id].1 } } let boss_salary = get_salary_recursive(boss_id, &mut salary_tree, &hierachy_tree); println!("{boss_salary}"); } }
猜字谜
题目描述
小王设计了一个简单的猜字谜游戏, 游戏的谜面是一个错误的单词, 比如 nesw
, 玩家需要猜出谜底库中正确的单词.
猜中的要求是, 对于某个谜面和谜底单词, 满足下面任一条件都表示猜中:
- 变换顺序以后一样的, 比如通过变换w和e的顺序,
nwes
跟news
是可以完全对应的 - 字母去重以后是一样的, 比如
woood
和wood
是一样的, 它们去重后都是wod
请你写一个程序帮忙在谜底库中找到正确的谜底.
谜面是多个单词, 都需要找到对应的谜底, 如果找不到的话, 返回 not found
.
输入描述
- 谜面单词列表, 以
,
分隔 - 谜底库单词列表, 以
,
分隔
输出描述
- 匹配到的正确单词列表, 以
,
分隔 - 如果找不到, 返回
not found
备注
- 单词的数量N的范围:
0 < N < 1000
- 词汇表的数量M的范围:
0 < M < 1000
- 单词的长度P的范围:
0 < P < 20
- 输入的字符只有小写英文字母, 没有其他字符
示例1
输入:
conection
connection,today
输出:
connection
示例2
输入:
bdni,wooood
bind,wrong,wood
输出:
bind,wood
题解
Python
def solution():
invalid_words = input().split(",")
words = input().split(",")
words_set = [(set(word), word) for word in words]
ans = []
for invalid_word in invalid_words:
invalid_chars = set(invalid_word)
matched_word = "Not found"
for chars_set, word in words_set:
if chars_set == invalid_chars:
matched_word = word
ans.append(matched_word)
print(",".join(ans))
def main():
solution()
if __name__ == "__main__":
main()
猜数字
题目描述
一个人设定一组四码的数字作为谜底, 另一方猜.
每猜一个数, 出数者就要根据这个数字给出提示, 提示以 XAYB
形式呈现, 直到猜中位置.
其中X表示位置正确的数的个数 (数字正确且位置正确), 而Y表示数字正确而位置不对的数的个数.
例如, 当谜底为8123
, 而猜谜者猜1052
时, 出题者必须提示0A2B
.
例如, 当谜底为5637
, 而猜谜者猜4931
时, 出题者必须提示1A0B
.
当前已知N组猜谜者猜的数字与提示, 如果答案确定, 请输出答案; 不确定则输出NA
.
输入描述
- 第一行输入一个正整数,
0 < N < 100
- 接下来N行, 每一行包含一个猜测的数字与提示结果
输出描述
输出最后的答案, 答案不确定则输出NA
.
示例1
输入:
6
4815 1A1B
5716 0A1B
7842 0A1B
4901 0A0B
8585 3A0B
8555 2A1B
输出:
3585
题解
Python
def main():
num_guess = int(input().strip())
# 读取输入, (guess_num, xAyB)
guess_list = [tuple(input().split()) for _ in range(num_guess)]
# 记录当前所有匹配的整数
# 当它为 1 时, 表示猜中了
# 当它大于1 时, 表示可能有多个数值符合, 所以无法确定真正的数值
valid_count = 0
ans = ""
# 暴力猜测, 生成 0000-9999 范围内所有的整数
# 然后计算它的模式 xAyB 是否跟给定的模式一致, 如果一致那就是这个整数
# 如果不一致, 就去遍历下一个
for num in range(10000):
# 将整数转换成四位的字符串
num_str = F"{num:04d}"
is_valid = True
# 遍历每一次猜测
for guess_num, pattern in guess_list:
# 数值和位置都正确
expected_pos_matches = int(pattern[0])
# 只有数值正确而位置错误的数值
expected_digit_matches = int(pattern[2])
# 用于记录数字和位置都相同的个数
pos_matches = 0
# 存储每个数字出现的次数
guess_arr = [0] * 10
num_arr = [0] * 10
for i in range(len(guess_num)):
# 遍历每个位上的数值
guess_digit = int(guess_num[i])
num_digit = int(num_str[i])
if guess_digit == num_digit:
# 位置和数值都相等
pos_matches += 1
else:
# 记录该数值出现的次数
guess_arr[guess_digit] += 1
num_arr[num_digit] += 1
# 接下来计算数字相同但位置不同
digit_matches = sum(min(guess_arr[i], num_arr[i]) for i in range(10))
# 结果不符, 不再遍历下个猜测的数字
if pos_matches != expected_pos_matches or digit_matches != expected_digit_matches:
is_valid = False
break
if is_valid:
valid_count += 1
ans = num_str
# 符合条件的数值比较多, 不再猜了
if valid_count > 1:
break
# 判断结果
if valid_count == 1:
print(ans)
else:
print("NA")
if __name__ == "__main__":
main()
最大报酬
题目描述
小明每周上班都会拿到自己的工作清单, 工作清单内包含 n 项工作, 每项工作都有对应的耗时时间 (单位 h) 和报酬, 工作的总报酬为所有已完成工作的报酬之和, 那么请你帮小明安排一下工作, 保证小明在指定的工作时间内工作收入最大化.
输入描述
- T 代表工作时长 (单位 h, 0 < T < 1000000)
- n 代表工作数量 (1 < n ≤ 3000)
- 接下来是 n 行, 每行包含两个整数 t, w
- t 代表该工作消耗的时长 (单位 h, t > 0), w 代表该项工作的报酬
输出描述
输出小明指定工作时长内工作可获得的最大报酬.
示例1
输入:
40 3
20 10
20 20
20 5
题解
Python
分糖果
题目描述
小明从糖果盒中随意抓一把糖果, 每次小明会取出一半的糖果分给同学们.
当糖果不能平均分配时, 小明可以选择从糖果盒中 (假设盒中糖果足够) 取出一个糖果或放回一个糖果.
小明最少需要多少次 (取出, 放回和平均分配均记一次), 能将手中糖果分至只剩一颗.
输入描述
抓取的糖果数 (< 10000000000)
输出描述
最少分至一颗糖果的次数
示例1
输入:
15
输出:
5
示例2
输入:
6
输出:
3
题解
动态规划的思路:
- 如果是奇数个糖果, 就有两种方法: 取一个再平分;放一个再平分. 我们可以分别计算它们的结果, 再求得其中的最小值
- 如果是偶数个糖果, 就先平均分一次
- 使用缓存存储中间结果, 加快运算
Python
import sys
def main():
cache = {1: 0}
# 缓存 + 递归
def get_minimum_times(num: int) -> int:
if num in cache:
return cache[num]
if num % 2 == 0:
# 如果是偶数个
# 平均分一次
times = 1 + get_minimum_times(num // 2)
cache[num] = times
return times
else:
# 如果是奇数个, 有两种方式:
# 取一个
times1 = 1 + get_minimum_times(num + 1)
# 放一个
times2 = 1 + get_minimum_times(num - 1)
# 求它们的最小值
min_times = min(times1, times2)
cache[num] = min_times
return min_times
sweet = int(input().strip())
times = get_minimum_times(sweet)
print(times)
if __name__ == "__main__":
main()
C++
#include <iostream>
#include <unordered_map>
// 缓存 + 递归
int get_minimum_times(int num, std::unordered_map<int, int>& cache) {
const auto iter = cache.find(num);
if (iter != cache.end()) {
return iter->second;
}
if (num % 2 == 0) {
// 如果是偶数个
// 平均分一次
const int times = 1 + get_minimum_times(num / 2, cache);
cache[num] = times;
return times;
} else {
// 如果是奇数个, 有两种方式:
// 取一个
const int times1 = 1 + get_minimum_times(num + 1, cache);
// 放一个
const int times2 = 1 + get_minimum_times(num - 1, cache);
// 求它们的最小值
const int min_times = std::min(times1, times2);
cache[num] = min_times;
return min_times;
}
}
int main() {
std::unordered_map<int, int> cache;
cache[1] = 0;
int num_candies = 0;
std::cin >> num_candies;
const int times = get_minimum_times(num_candies, cache);
std::cout << times << std::endl;
return 0;
}
Rust
use std::collections::HashMap; use std::io::{stdin, BufRead}; fn main() { // 缓存 + 递归 fn get_minimum_times(num: usize, cache: &mut HashMap<usize, usize>) -> usize { if let Some(value) = cache.get(&num) { return *value; } if num % 2 == 0 { // 如果是偶数个 // 平均分一次 let times = 1 + get_minimum_times(num / 2, cache); cache.insert(num, times); times } else { // 如果是奇数个, 有两种方式: // 取一个 let times1 = 1 + get_minimum_times(num + 1, cache); // 放一个 let times2 = 1 + get_minimum_times(num - 1, cache); // 求它们的最小值 let min_times = times1.min(times2); cache.insert(num, min_times); min_times } } let mut cache = HashMap::from([(1, 0)]); let mut line = String::new(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let num_candies: usize = line.trim().parse().unwrap(); let times = get_minimum_times(num_candies, &mut cache); println!("{times}"); }
日志采集系统
题目描述
日志采集是运维系统的的核心组件. 日志是按行生成, 每行记做一条, 由采集系统分批上报.
- 如果上报太频繁, 会对服务端造成压力
- 如果上报太晚, 会降低用户的体验
- 如果一次上报的条数太多, 会导致超时失败
为此, 项目组设计了如下的上报策略:
- 每成功上报一条日志, 奖励1分
- 每条日志每延迟上报1秒, 扣1分
- 积累日志达到100条, 必须立即上报
给出日志序列, 根据该规则, 计算首次上报能获得的最多积分数.
输入描述
按时序产生的日志条数 T1,T2…Tn, 其中 1 <= n <= 1000
, 0 <= Ti <= 100
输出描述
首次上报最多能获得的积分数.
示例1
输入:
1 98 1
输出:
98
说明:
- T1 时刻上报得 1 分
- T2 时刻上报得98分, 最大
- T3 时刻上报得 0 分
示例2
输入:
50 60 1
输出:
50
说明:
如果第1个时刻上报, 获得积分50. 如果第2个时刻上报, 最多上报100条, 前50条延迟上报1s, 每条扣除1分, 共获得积分为 100-50=50
示例3
输入:
3 7 40 10 60
输出:
37
说明:
- T1时刻上报得3分
- T2时刻上报得7分
- T3时刻上报得37分, 最大
- T4时刻上报得-3分
- T5时刻上报, 因为已经超了100条限制, 所以只能上报100条, 得-23分
题解
最左侧冗余覆盖子串
题目描述
给定两个字符串s1和s2和正整数K, 其中s1长度为n1, s2长度为n2, 在s2中选一个子串, 满足:
- 该子串长度为 n1+k
- 该子串中包含s1中全部字母
- 该子串每个字母出现次数不小于s1中对应的字母
我们称s2以长度k冗余覆盖s1, 给定s1, s2, k, 求最左侧的s2以长度k冗余覆盖s1的子串的首个元素的下标,
如果没有返回 -1
.
输入描述
输入三行, 第一行为s1, 第二行为s2, 第三行为k, s1和s2只包含小写字母.
备注:
0 ≤ len(s1) ≤ 1000000
0 ≤ len(s2) ≤ 20000000
0 ≤ k ≤ 1000
输出描述
最左侧的s2以长度k冗余覆盖s1的子串首个元素下标, 如果没有返回 -1
.
示例1
输入:
ab
aabcd
1
输出:
0
说明: 子串aab和abc符合要求, 由于aab在abc的左侧, 因此输出aab的下标 0
.
示例2
输入:
abc
dfs
10
输出:
-1
题解
Python
import string
def solution():
# 读取输入
s1 = input()
s2 = input()
k = int(input())
assert 0 < k
# 统计s1中各个字母出现的次数
s1_chars = dict((char, 0) for char in string.ascii_lowercase)
for char in s1:
s1_chars[char] += 1
# 滑动窗口内各个字母出现的次数
window_chars = dict((char, 0) for char in string.ascii_lowercase)
left = 0
right = 0
s1_chars_left = len(s1)
while right < len(s2):
# 更新窗口内各个字母的次数
right_char = s2[right]
window_chars[right_char] += 1
if window_chars[right_char] <= s1_chars[right_char]:
s1_chars_left -= 1
# 如果窗口太大, 将窗口左侧向右移
if right - left + 1 > len(s1) + k:
left_char = s2[left]
if window_chars[left_char] <= s1_chars[left_char]:
s1_chars_left += 1
# 将左侧字符从窗口字母计数中移除
window_chars[left_char] -= 1
left += 1
# 如果 s1 中没有剩下的字符待检查, 就说明找到了子串
if s1_chars_left == 0:
return left
# 将窗口右侧向右移
right += 1
return -1
def main():
ans = solution()
print(ans)
if __name__ == "__main__":
main()
增强的strstr
题目描述
C 语言有一个库函数 char *strstr(const char *haystack, const char *needle)
, 实现在字符串 haystack
中查找第一次出现字符串 needle
的位置, 如果未找到则返回 null
.
现要求实现一个 strstr
的增强函数, 可以使用带可选段的字符串来模糊查询, 与 strstr
一样返回首次查找到的字符串位置.
可选段使用 []
标识, 表示该位置是可选段中任意一个字符即可满足匹配条件.
比如a[bc]
表示可以匹配ab
或ac
.
注意目标字符串中可选段可能出现多次.
输入描述
与 strstr
函数一样, 输入参数是两个字符串指针, `分别是源字符串和目标字符串.
输出描述
与 strstr
函数不同, 返回的是源字符串中, 匹配子字符串相对于源字符串地址的偏移 (从0开始算), 如果没有匹配返回 -1
.
补充说明: 源字符串中必定不包含 []
; 目标字符串中[]
必定成对出现, 且不会出现嵌套.
输入的字符串长度在 [1,100]
之间.
示例1
输入:
abcd
b[cd]
输出:
1
题解
Python
import re
def main():
# 读取输入
source = input()
assert 1 <= len(source) <= 100
pattern = input()
# 使用正则库
re_pattern = re.compile(pattern)
matches = re_pattern.search(source)
if matches:
print(matches.start())
else:
print(-1)
if __name__ == "__main__":
main()
报文响应时间
题目描述
IGMP 协议中, 有一个字段称作最大响应时间 (Max Response Time), HOST收到查询报文, 解折出 MaxResponseTime 字段后, 需要在
(0,MaxResponseTime]
时间(秒)内选取随机时间回应一个响应报文.
如果在随机时间内收到一个新的查询报文, 则会根据两者时间的大小, 选取小的一方刷新回应时间.
最大响应时间有如下计算方式:
- 当
MaxRespCode < 128
时,MaxRespTime = MaxRespCode
- 当
MaxRespCode ≥ 128
时,MaxRespTime = (mant | 0x10) << (exp + 3)
注:
exp
最大响应时间的高5~7位mant
为最大响应时间的低4位
其中接收到的 MaxRespCode
最大值为 255, 以上出现所有字段均为无符号数.
现在我们认为 HOST 收到查询报文时, 选取的随机时间必定为最大值, 现给出 HOST 收到查询报文个数 C, HOST 收到该报文的时间T, 以及查询报文的最大响应时间字段值 M, 请计算出 HOST 发送响应报文的时间.
输入描述
第一行为查询报文个数 C, 后续每行分别为 HOST 收到报文时间 T, 及最大响应时间M, 以空格分割.
输出描述
HOST 发送响应报文的时间.
备注: 用例确定只会发送一个响应报文, 不存在计时结束后依然收到查询报文的情况.
示例1
输入:
3
0 20
1 10
8 20
输出:
11
说明:
- 收到3个报文
- 第0秒收到第1个报文, 响应时间为20秒, 则要到
0+20=20
秒响应 - 第1秒收到第2个报文, 响应时间为10秒, 则要到
1+10=11
秒响应, 与上面的报文的响应时间比较获得响应时间最小为11秒 - 第8秒收到第3个报文, 响应时间为20秒, 则要到
8+20=28
秒响应, 与第上面的报文的响应时间比较获得响应时间最小为11秒 - 最终得到最小响应报文时间为11秒
示例2
输入:
2
0 255
200 60
输出:
260
说明:
- 收到2个报文
- 第0秒收到第1个报文, 响应时间为255秒, 则要到
(15 | 0x10) << (7 + 3)= 31744
秒响应, (mant = 15,exp = 7) - 第200秒收到第2个报文, 响应时间为60秒, 则要到
200+60-260
秒响应, 与第上面的报文的响应时间比较获得响应时间最小为260秒 - 最终得到最小响应报文时间为260秒
题解
这个问题要搞明白如何进行位运算, 然后注意边界条件即可.
Python
import sys
def main():
def get_resp_time(req_time):
if req_time < 128:
return req_time
else:
mant = req_time & 0b1111
exp = (req_time >> 4 ) & 0b0110
return (mant | 0x10) << (exp + 3)
num_request = int(input().strip())
# 读取所有的请求报文
req_list = []
for line in sys.stdin.readlines():
parts = line.split()
delay = int(parts[0])
req_time = int(parts[1])
req_list.append((delay, req_time))
assert num_request == len(req_list)
# 计算每个请求报文的响应时间, 并找到最小的值
min_resp_time = 2 ** 32
for delay, req_time in req_list:
resp_time = get_resp_time(req_time)
abs_resp_time = delay + resp_time
min_resp_time = min(min_resp_time, abs_resp_time)
print(min_resp_time)
if __name__ == "__main__":
main()
C++
#include <cassert>
#include <climits>
#include <iostream>
#include <vector>
int get_resp_time(int req_time) {
if (req_time < 128) {
return req_time;
} else {
int mant = req_time & 0b1111;
int exp = (req_time >> 4) & 0b0110;
return (mant | 0x10) << (exp + 3);
}
}
int main() {
// 读取所有的请求报文
int num_request = 0;
std::cin >> num_request;
std::vector<std::tuple<int, int>> req_list;
int delay = 0;
int req_time = 0;
while (std::cin >> delay >> req_time) {
req_list.emplace_back(delay, req_time);
}
assert(num_request == req_list.size());
// 计算每个请求报文的响应时间, 并找到最小的值
int min_resp_time = INT_MAX;
for (const auto tuple: req_list) {
int delay = std::get<0>(tuple);
int req_time = std::get<1>(tuple);
int resp_time = get_resp_time(req_time);
int abs_resp_time = delay + resp_time;
min_resp_time = std::min(min_resp_time, abs_resp_time);
}
std::cout << min_resp_time << std::endl;
return 0;
}
Rust
use std::io::{stdin, BufRead}; fn get_resp_time(req_time: i32) -> i32 { if req_time < 128 { req_time } else { let mant: i32 = req_time & 0b1111; let exp: i32 = (req_time >> 4) & 0b0110; (mant | 0x10) << (exp + 3) } } fn main() { let mut line = String::new(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let num_request: usize = line.trim().parse().unwrap(); // 读取所有的请求报文 let mut req_list = Vec::with_capacity(num_request); for line in stdin().lock().lines() { let line = line.unwrap(); let mut parts = line.split_ascii_whitespace(); let delay: i32 = parts.next().unwrap().parse().unwrap(); let req_time: i32 = parts.next().unwrap().parse().unwrap(); req_list.push((delay, req_time)); } assert_eq!(num_request, req_list.len()); // 计算每个请求报文的响应时间, 并找到最小的值 let mut min_resp_time = i32::MAX; for (delay, req_time) in req_list { let resp_time = get_resp_time(req_time); let abs_resp_time = delay + resp_time; min_resp_time = min_resp_time.min(abs_resp_time); } println!("{min_resp_time}"); }
连续字母长度
题目描述
给定一个字符串, 只包含大写字母, 求在包含同一字母的子串中, 长度第 k 长的子串的长度, 相同字母只取最长的那个子串.
输入描述
第一行有一个子串 (1<长度<=100)
, 只包含大写字母.
输出描述
输出连续出现次数第k多的字母的次数.
示例1
输入:
AAAAHHHBBCDHHHH
3
输出:
2
说明:
- 同一字母连续出现的最多的是A和H, 四次
- 第二多的是H, 3次, 但是H已经存在4个连续的, 故不考虑
- 下个最长子串是BB, 所以最终答案应该输出2
示例2
输入:
AABAAA
2
输出:
1
说明:
- 同一字母连续出现的最多的是A, 三次
- 第二多的还是A, 两次, 但A已经存在最大连续次数三次, 故不考虑
- 下个最长子串是B, 所以输出1
示例3
输入:
ABC
4
输出:
-1
示例4
输入:
ABC
2
输出:
1
题解
Python
import sys
def main():
string = input().strip()
k = int(input().strip())
# 先遍历字符串, 分隔出连续相同字符, 然后统计其个数, 存放到计数字典中
current_char = 'A'
current_char_count = 0
char_dict = dict()
string_len = len(string)
for char in string:
# 如果当前的字符与上个字符不相同
if current_char != char:
# 保存到字典中
if current_char_count > 0:
# 如果该字符在字典中已经存在, 则只保存最大连续数
last_count = char_dict.get(current_char)
if last_count:
char_dict[current_char] = max(last_count, current_char_count)
else:
char_dict[current_char] = current_char_count
# 重置上个字符及其计数
current_char = char
current_char_count = 1
else:
current_char_count += 1
# 处理最后一个字符
if current_char_count > 0:
# 如果该字符在字典中已经存在, 则只保存最大连续数
last_count = char_dict.get(current_char)
if last_count:
char_dict[current_char] = max(last_count, current_char_count)
else:
char_dict[current_char] = current_char_count
# 将字典转换成列表
word_list = []
for (char, count) in char_dict.items():
word_list.append((count, char))
# 基于最大连续数进行排序, 从高到低
word_list.sort(key = lambda pair: pair[0], reverse = True)
#print(word_list)
# 并找到第 k 个字符, 注意下标从0开始计数, 而k是从1开始的
if k <= len(word_list):
print(word_list[k - 1][0])
else:
print(-1)
if __name__ == "__main__":
main()
Rust
use std::cmp::Reverse; use std::collections::HashMap; use std::io::{stdin, BufRead}; fn main() { // 读取输入 let mut s = String::new(); let ret = stdin().lock().read_line(&mut s); assert!(ret.is_ok()); let mut k_str = String::new(); let ret = stdin().lock().read_line(&mut k_str); assert!(ret.is_ok()); let k: usize = k_str.trim().parse().unwrap(); // 先遍历字符串, 分隔出连续相同字符, 然后统计其个数, 存放到计数字典中 let mut current_char: char = 'A'; let mut current_char_count: usize = 0; let mut char_dict: HashMap<char, usize> = HashMap::new(); for chr in s.trim().chars() { // 如果当前的字符与上个字符不相同 if current_char != chr { // 保存到字典中 if current_char_count > 0 { // 如果该字符在字典中已经存在, 则只保存最大连续数 if let Some(last_count) = char_dict.get_mut(¤t_char) { *last_count = current_char_count.max(*last_count); } else { char_dict.insert(current_char, current_char_count); } } // 重置上个字符及其计数 current_char = chr; current_char_count = 1; } else { current_char_count += 1; } } // 处理最后一个字符 if current_char_count > 0 { // 如果该字符在字典中已经存在, 则只保存最大连续数 if let Some(last_count) = char_dict.get_mut(¤t_char) { *last_count = current_char_count.max(*last_count); } else { char_dict.insert(current_char, current_char_count); } } // 将字典转换成列表 let mut word_list: Vec<(char, usize)> = char_dict.into_iter().collect(); // 基于最大连续数进行排序, 从高到低 word_list.sort_by_key(|pair| Reverse(pair.1)); //println!("{word_list:?}"); // 并找到第 k 个字符, 注意下标从0开始计数, 而k是从1开始的 if k <= word_list.len() { println!("{}", word_list[k - 1].1); } else { println!("-1"); } }
C++
#include <cassert>
#include <algorithm>
#include <iostream>
#include <unordered_map>
#include <vector>
int main() {
// 读取输入
std::string s;
std::getline(std::cin, s);
int k = 0;
std::cin >> k;
// 先遍历字符串, 分隔出连续相同字符, 然后统计其个数, 存放到计数字典中
char current_char = 'A';
int current_char_count = 0;
std::unordered_map<char, int> char_dict;
for (char chr : s) {
// 如果当前的字符与上个字符不相同
if (current_char != chr) {
// 保存到字典中
if (current_char_count > 0) {
// 如果该字符在字典中已经存在, 则只保存最大连续数
auto iter = char_dict.find(current_char);
if (iter != char_dict.end()) {
iter->second = std::max(iter->second, current_char_count);
} else {
char_dict.emplace(current_char, current_char_count);
}
}
// 重置上个字符及其计数
current_char = chr;
current_char_count = 1;
} else {
current_char_count += 1;
}
}
// 处理最后一个字符
if (current_char_count > 0) {
// 如果该字符在字典中已经存在, 则只保存最大连续数
auto iter = char_dict.find(current_char);
if (iter != char_dict.end()) {
iter->second = std::max(iter->second, current_char_count);
} else {
char_dict.emplace(current_char, current_char_count);
}
}
// 将字典转换成列表
std::vector<std::tuple<int, char>> word_list;
for (const auto tuple : char_dict) {
word_list.emplace_back(std::get<1>(tuple), std::get<0>(tuple));
}
// 基于最大连续数进行排序, 从高到低
std::sort(word_list.begin(), word_list.end(),
[](const std::tuple<int, char>& a, const std::tuple<int, char>& b) {
return std::get<0>(a) > std::get<0>(b);
});
//for (const auto& item : word_list) {
// std::cout << std::get<1>(item) << ":" << std::get<0>(item) << std::endl;
//}
// 并找到第 k 个字符, 注意下标从0开始计数, 而k是从1开始的
if (k <= word_list.size()) {
std::cout << std::get<0>(word_list[k - 1]) << std::endl;
} else {
std::cout << "-1" << std::endl;
}
return 0;
}
最长连续子序列
题目描述
有N个正整数组成的一个序列, 给定整数sum, 求长度最长的连续子序列, 使他们的和等于sum, 返回此子序列的长度.
如果没有满足要求的序列, 返回-1
.
输入描述
- 第一行输入是: N个正整数组成的一个序列
- 第二行输入是: 给定整数sum
输出描述
最长的连续子序列的长度
备注:
- 输入序列仅由数字和英文逗号构成, 数字之间采用英文逗号分隔
- 序列长度:
1 <= N <= 200
- 输入序列不考虑异常情况
示例1
输入:
1,2,3,4,2
6
输出:
3
说明: 1,2,3和4, 2两个序列均能满足要求, 所以最长的连续序列为1,2,3, 因此结果为3.
示例2
输入:
1,2,3,4,2
20
输出:
-1
题解
Python
# 滑动窗口
def main():
nums = list(map(int, input().split(",")))
expected_sum = int(input())
assert 1 <= len(nums) <= 200
assert 1 <= expected_sum
# 最长子序的长度
subarray_max_len = -1
# 用于控制窗口左侧两侧的位置
left = 0
right = 0
# 计算当前子序列的和
current_sum = 0
while left <= right and right < len(nums):
if current_sum < expected_sum:
# 子序列的和太小, 将窗口右侧向右移
current_sum += nums[right]
right += 1
elif current_sum == expected_sum:
# 和相等, 计算当前的子序列长度
current_length = right - left
subarray_max_len = max(subarray_max_len, current_length)
current_sum += nums[right]
right += 1
else:
# 子序列的和太大, 将窗口左侧向右移
current_sum -= nums[left]
left += 1
print(subarray_max_len)
if __name__ == "__main__":
main()
Rust
#![allow(unused)] fn main() { use std::cmp::Ordering; use std::io::{stdin, BufRead}; // 滑动窗口 fn solution() { // 读取输入 let mut line = String::new(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let nums: Vec<i32> = line.trim().split(',').map(|x| x.parse().unwrap()).collect(); line.clear(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let expected_sum: i32 = line.trim().parse().unwrap(); // 最长子序的长度 let mut subarray_max_len: usize = 0; // 用于控制窗口左侧两侧的位置 let mut left: usize = 0; let mut right: usize = 0; // 计算当前子序列的和 let mut current_sum: i32 = 0; while left <= right && right < nums.len() { match current_sum.cmp(&expected_sum) { Ordering::Less => { // 子序列的和太小, 将窗口右侧向右移 current_sum += nums[right]; right += 1; } Ordering::Equal => { // 和相等, 计算当前的子序列长度 let current_length = right - left; subarray_max_len = subarray_max_len.max(current_length); // 并把窗口右侧向右移 current_sum += nums[right]; right += 1; } Ordering::Greater => { // 子序列的和太大, 将窗口左侧向右移 current_sum -= nums[left]; left += 1; } } } // 打印结果 println!("{subarray_max_len}"); } }
C++
#include <cassert>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
// 滑动窗口
void solution() {
// 读取输入
std::string line;
std::cin >> line;
std::stringstream ss(line);
std::string part;
std::vector<int> nums;
while (std::getline(ss, part, ',')) {
const int num = std::stoi(part);
nums.push_back(num);
}
assert(1 <= nums.size() && nums.size() <= 200);
int expected_sum;
std::cin >> expected_sum;
assert(1 <= expected_sum);
// 最长子序的长度
int subarray_max_len = -1;
// 用于控制窗口左侧两侧的位置
int left = 0;
int right = 0;
// 计算当前子序列的和
int current_sum = 0;
while (left <= right && right < nums.size()) {
if (current_sum < expected_sum) {
// 子序列的和太小, 将窗口右侧向右移
current_sum += nums[right];
right += 1;
} else if (current_sum == expected_sum) {
// 和相等, 计算当前的子序列长度
const int current_length = right - left;
subarray_max_len = std::max(subarray_max_len, current_length);
current_sum += nums[right];
right += 1;
} else {
// 子序列的和太大, 将窗口左侧向右移
current_sum -= nums[left];
left += 1;
}
}
// 打印结果
std::cout << subarray_max_len << std::endl;
}
计算面积/绘图机器
题目描述
绘图机器的绘图笔初始位置在原点 (0,0)
机器启动后按照以下规则来进行绘制直线.
- 尝试沿着横线坐标正向绘制直线直到给定的终点E
- 期间可以通过指令在纵坐标轴方向进行偏移, offsetY为正数表示正向偏移, 为负数表示负向偏移
给定的横坐标终点值E 以及若干条绘制指令, 请计算绘制的直线和横坐标轴以及 x=E 的直线组成的图形面积.
输入描述
- 首行为两个整数 N 和 E
- 表示有N条指令,机器运行的横坐标终点值E
- 接下来N行, 每行两个整数表示一条绘制指令x offsetY
- 用例保证横坐标x以递增排序的方式出现
- 且不会出现相同横坐标x
取值范围:
- 0 < N <= 10000
- 0 <= x <= E <= 20000
- -10000 <= offsetY <= 10000
输出描述
一个整数表示计算得到的面积, 用例保证结果范围在0到 4294967295 之内.
示例1
输入:
4 10
1 1
2 1
3 1
4 -2
输出:
12
说明:
示例2
输入:
2 4
0 1
2 -2
输出:
4
说明:
题解
Python
def main():
# 读取输入
# 遍历每个坐标点
# 计算相邻坐标点之间形成的矩形面积
# 然后计算所有面积之和, 就是结果
parts = input().split()
assert len(parts) == 2
# 移动的坐标点数
num_points = int(parts[0])
# 终点E所在的X坐标
end_x = int(parts[1])
points = []
last_y = 0
# 遍历所有的输入坐标, 并计算它们的绝对坐标
for i in range(num_points):
parts = input().split()
assert len(parts) == 2
x = int(parts[0])
offset_y = int(parts[1])
y = last_y + offset_y
points.append((x, y))
last_y = y
# 移动的总面积
total_areas = 0
# 出发点是原点
last_point = (0, 0)
# 将最后一个点也加入进来
end_point = (end_x, 0)
points.append(end_point)
# 遍历所有的点
for point in points:
# 机器人移动方式是: 先横向移动到 point.x, 再纵向移动到 point.y
# 矩形面积是 dx * dy
dx = abs(point[0] - last_point[0])
#dy = abs_int(point[1] - last_point[1])
dy = abs(last_point[1])
total_areas += dx * dy
last_point = point
print(total_areas)
if __name__ == "__main__":
main()
敏感字段加密
题目描述
给定一个由多个命令字组成的命令字符串:
- 字符串长度小于等于127字节, 只包含大小写字母, 数字, 下划线和偶数个双引号
- 命令字之间以一个或多个下划线
_
进行分割 - 可以通过两个双引号
""
来标识包含下划线_
的命令字或空命令字 (仅包含两个双引号的命令字), 双引号不会在命令字内部出现
请对指定索引的敏感字段进行加密, 替换为 ******
(6个*), 并删除命令字前后多余的下划线_
.
如果无法找到指定索引的命令字, 输出字符串 ERROR
.
输入描述
输入为两行, 第一行为命令字索引K (从0开始), 第二行为命令字符串S.
输出描述
输出处理后的命令字符串, 如果无法找到指定索引的命令字, 输出字符串 ERROR
.
示例1
输入:
1
password__a12345678_timeout_100
输出:
password_******_timeout_100
示例2
输入:
2
aaa_password_"a12_45678"_timeout__100_""_
输出:
aaa_password_******_timeout_100_""
题解
出租车计费/靠谱的车
题目描述
程序员小明打了一辆出租车去上班. 出于职业敏感, 他注意到这辆出租车的计费表有点问题, 总是偏大.
出租车司机解释说他不喜欢数字4, 所以改装了计费表, 任何数字位置遇到数字4就直接跳过, 其余功能都正常.
比如:
- 23再多一块钱就变为25
- 39再多一块钱变为50
- 399再多一块钱变为500
小明识破了司机的伎俩, 准备利用自己的学识打败司机的阴谋. 给出计费表的表面读数, 返回实际产生的费用.
输入描述
只有一行, 数字N, 表示里程表的读数, 1 <= N <= 888888888.
输出描述
一个数字, 表示实际产生的费用, 以回车结束.
示例1
输入:
5
输出:
4
说明: 5表示计费表的表面读数, 4表示实际产生的费用其实只有4块钱.
示例2
输入:
17
输出:
15
说明: 17表示计费表的表面读数, 15表示实际产生的费用其实只有15块钱.
示例3
输入:
100
输出:
81
说明: 100表示计费表的表面读数, 81表示实际产生的费用其实只有81块钱.
题解
Python
def main():
# 读取输入
line = input().strip()
# 考虑使用9进制
# 遍历所有的输入字符, 将它转换成数字
real_num = 0
for char in line:
digit = int(char)
# 原先的数字跳过了4, 我们把它还原
if digit > 4:
digit -= 1
# 将9进制转换成10进制
real_num = real_num * 9 + digit
print(real_num)
if __name__ == "__main__":
main()
分苹果
题目描述
A, B两个人把苹果分为两堆, A希望按照他的计算规则等分苹果, 他的计算规则是按照二进制加法计算,
并且不计算进位 12+5=9
(1100 + 0101 = 9).
B的计算规则是十进制加法, 包括正常进位, B希望在满足A的情况下获取苹果重量最多.
输入苹果的数量和每个苹果重量, 输出满足A的情况下B获取的苹果总重量.
如果无法满足A的要求, 输出 -1
.
数据范围:
- 1 <= 总苹果数量 <= 20000
- 1 <= 每个苹果重量 <= 10000
输入描述
- 输入第一行是苹果数量, 3
- 输入第二行是每个苹果重量, 3 5 6
输出描述
输出第一行是B获取的苹果总重量 11.
示例1
输入:
3
3 5 6
输出:
11
示例2
输入:
8
7258 6579 2602 6716 3050 3564 5396 1773
输出:
35165
题解
Python
def main():
# 读取输入
# 计算所有苹果的异或和, 如果不为0, 则没有办法按照A的方法来分, 直接返回
# 计算所有苹果的重量之和
# 找到最小重量的苹果并把它分给A, 剩下的都分给B
num_apples = int(input())
apple_weights = list(map(int, input().split()))
assert len(apple_weights) == num_apples
assert num_apples <= 20000
MAX_WEIGHT = 10000
xor_sum = 0
total_weight = 0
min_weight = MAX_WEIGHT
for weight in apple_weights:
xor_sum = xor_sum ^ weight
total_weight += weight
min_weight = min(min_weight, weight)
if xor_sum != 0:
print(-1)
else:
# 将 min_weight 那个苹果分给A, 剩下的都给B
remains = total_weight - min_weight
print(remains)
if __name__ == "__main__":
main()
字符串变换最小字符串
题目描述
给定一个字符串s, 最多只能进行一次变换, 返回变换后能得到的最小字符串 (按照字典序进行比较).
变换规则: 交换字符串中任意两个不同位置的字符.
输入描述
一串小写字母组成的字符串s.
备注:
- s是都是小写字符组成
- 1 <= s.length <= 1000
输出描述
一串小写字母组成的字符串s.
示例1
输入:
abcdef
输出:
abcdef
说明: abcdef已经是最小字符串, 不需要交换.
示例2
输入:
bcdefa
输出:
说明: a和b进行位置交换, 可以得到最小字符串.
题解
字符串分割转换
题目描述
给定一个非空字符串S, 其被N个-
分隔成 N + 1 的子串, 给定正整数K, 要求除第一个子串外, 其余的子串每K个字符组成新的子串, 并用
-
分隔.
对于新组成的每一个子串, 如果它含有的小写字母比大写字母多, 则将这个子串的所有大写字母转换为小写字母.
反之, 如果它含有的大写字母比小写字母多, 则将这个子串的所有小写字母转换为大写字母; 大小写字母的数量相等时, 不做转换,
输入描述
输入为两行, 第一行为参数K, 第二行为字符串S.
输出描述
输出转换后的字符串.
示例1
输入:
3
12abc-abCABc-4aB@
输出:
12abc-abc-ABC-4aB-@
说明:
- 子串为12abc, abCABc, 4aB@, 第一个子串保留
- 后面的子串每3个字符一组为abC, ABc, 4aB, @
- abC中小写字母较多, 转换为abc
- ABc中大写字母较多, 转换为ABC
- 4aB中大小写字母都为1个, 不做转换
- @中没有字母, 连起来即
12abc-abc-ABC-4aB-@
示例2
输入:
12
12abc-abCABc-4aB@
输出:
12abc-abCABc4aB@
简单的自动曝光/平均像素值
题目描述
一个图像有n个像素点, 存储在一个长度为n的数组img里, 每个像素点的取值范围 [0,255]
的正整数.
请你给图像每个像素点值加上一个整数k (可以是负数), 得到新图newImg, 使得新图newImg的所有像素平均值最接近中位值128.
请输出这个整数k.
输入描述
n个整数, 中间用空格分开.
备注:
- 1 <= n <= 100
- 如有多个整数k都满足, 输出小的那个k
- 新图的像素值会自动截取到
[0,255]
范围 - 当新像素值 < 0, 其值会更改为0; 当新像素值 > 255, 其值会更改为255
例如newImg=-1 -2 256
, 会自动更改为 0 0 255
输出描述
一个整数k.
示例1
输入:
129 130 129 130
输出:
-2
说明: -1的均值128.5, -2的均值为127.5, 输出较小的数-2.
示例2
输入:
0 0 0 0
输出:
128
说明: 四个像素值都为0.
题解
Python
def main():
# 读取输入
img = list(map(int, input().split()))
num_pixels = len(img)
assert 1 <= num_pixels <= 100
# 平均值与 128 之间的差距
min_diff = 255
# 与平均值与 128 之间的差距最小时的 k 值
best_k = 0
# 然后遍历所有可能的k值, 计算经它调整之后所有像素的平均值
for k in range(-127, 128):
# 计算当前的平均值
sum_pixels = 0
for pixel in img:
# 遍历每个像素点, 计算新的像素值
# 注意像素值的范围是 [0, 255]
new_pixel = pixel + k
new_pixel = min(new_pixel, 255)
new_pixel = max(new_pixel, 0)
sum_pixels += new_pixel
avg_pixel = sum_pixels / num_pixels
pixel_diff = abs(avg_pixel - 128)
if pixel_diff < min_diff:
min_diff = pixel_diff
best_k = k
elif pixel_diff == min_diff and best_k != 0:
# 如果平均值相等, 那就选择较小的那个
best_k = min(best_k, k)
# 打印结果
print(best_k)
if __name__ == "__main__":
main()
计算三叉搜索树的高度
题目描述
定义构造三叉搜索树规则如下:
每个节点都存有一个数, 当插入一个新的数时, 从根节点向下寻找, 直到找到一个合适的空节点插入.
查找的规则是:
- 如果数小于节点的数减去500, 则将数插入节点的左子树
- 如果数大于节点的数加上500, 则将数插入节点的右子树
- 否则, 将数插入节点的中子树
给你一系列数, 请按以上规则, 按顺序将数插入树中, 构建出一棵三叉搜索树, 最后输出树的高度.
输入描述
第一行为一个数 N, 表示有 N 个数, 1 ≤ N ≤ 10000
第二行为 N 个空格分隔的整数, 每个数的范围为 [1,10000]
输出描述
输出树的高度 (根节点的高度为1)
示例1
输入:
5
5000 2000 5000 8000 1800
输出:
3
说明:
示例2
输入:
3
5000 4000 3000
输出:
3
说明:
示例3
输入:
9
5000 2000 5000 8000 1800 7500 4500 1400 8100
输出:
4
说明:
题解
Python
class TreeNode:
def __init__(self, value):
self.value = value
self.left = None
self.middle = None
self.right = None
# 向当前节点的子节点插入新的值
# 如果当前节点的子节点不存在, 就创建它
# 如果对应的子节点已经存在, 就在子节点中完成插入操作
def insert(self, value):
if self.value - value > 500:
# 去左子节点
if self.left:
self.left.insert(value)
else:
self.left = TreeNode(value)
elif value - self.value > 500:
# 去右子节点
if self.right:
self.right.insert(value)
else:
self.right = TreeNode(value)
else:
# 去中间子节点
if self.middle:
self.middle.insert(value)
else:
self.middle = TreeNode(value)
# 构造三叉搜索树
def build_tree(array: list[int]) -> TreeNode:
# 创建根节点
root = TreeNode(array[0])
for value in array[1:]:
root.insert(value)
return root
# 递归访问所有节点
def tree_height(root: TreeNode) -> int:
if not root:
return 0
left_height = tree_height(root.left)
middle_height = tree_height(root.middle)
right_height = tree_height(root.right)
child_height = max(left_height, middle_height, right_height)
return 1 + child_height
def main():
# 读取输入
# 读取节点个数
num_nodes = int(input())
assert 1 <= num_nodes <= 10000
# 读取所有节点的值, 存放到数组
array = list(map(int, input().split()))
assert len(array) == num_nodes
# 构造三叉树
root = build_tree(array)
# 获取树的最大高度
height = tree_height(root)
# 打印结果
print(height)
if __name__ == "__main__":
main()
Rust
use std::io::{stdin, BufRead}; pub struct TreeNode { value: i32, left: Option<Box<Self>>, middle: Option<Box<Self>>, right: Option<Box<Self>>, } impl TreeNode { pub fn new(value: i32) -> Self { Self { value, left: None, middle: None, right: None, } } fn boxed_new(value: i32) -> Option<Box<Self>> { Some(Box::new(Self { value, left: None, middle: None, right: None, })) } // 向当前节点的子节点插入新的值 // 如果当前节点的子节点不存在, 就创建它 // 如果对应的子节点已经存在, 就在子节点中完成插入操作 pub fn insert(&mut self, value: i32) { if self.value - value > 500 { // 去左子节点 if let Some(left) = &mut self.left { left.insert(value); } else { self.left = Self::boxed_new(value); } } else if value - self.value > 500 { // 去右子节点 if let Some(right) = &mut self.right { right.insert(value); } else { self.right = TreeNode::boxed_new(value); } } else { // 去中间子节点 if let Some(middle) = &mut self.middle { middle.insert(value); } else { self.middle = TreeNode::boxed_new(value); } } } } // 构造三叉搜索树 fn build_tree(array: &[i32]) -> TreeNode { assert!(!array.is_empty()); // 创建根节点 let mut root = TreeNode::new(array[0]); for &value in &array[1..] { root.insert(value); } root } // 递归访问所有节点 fn tree_height(root: &Option<Box<TreeNode>>) -> usize { if let Some(root) = root.as_ref() { let left_height = tree_height(&root.left); let middle_height = tree_height(&root.middle); let right_height = tree_height(&root.right); let child_height = left_height.max(middle_height.max(right_height)); 1 + child_height } else { 0 } } fn main() { // 读取输入 // 读取节点个数 let mut line = String::new(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let num_nodes: usize = line.trim().parse().unwrap(); assert!((1..=10000).contains(&num_nodes)); // 读取所有节点的值, 存放到数组 line.clear(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let array: Vec<i32> = line .trim() .split_ascii_whitespace() .map(|s| s.parse().unwrap()) .collect(); assert_eq!(array.len(), num_nodes); // 构造三叉树 let root = build_tree(&array); let root = Some(Box::new(root)); // 获取树的最大高度 let height: usize = tree_height(&root); // 打印结果 println!("{height}"); }
补种未成活胡杨
题目描述
近些年来, 我国防沙治沙取得显著成果. 某沙漠新种植N棵胡杨 (编号1-N), 排成一排.
一个月后, 有M棵胡杨未能成活.
现可补种胡杨K棵, 请问如何补种 (只能补种, 不能新种), 可以得到最多的连续胡杨树?
输入描述
- N 总种植数量, 1 <= N <= 100000
- M 未成活胡杨数量, M 个空格分隔的数, 按编号从小到大排列, 1 <= M <= N
- K 最多可以补种的数量, 0 <= K <= M
输出描述
最多的连续胡杨棵树.
示例1
输入:
5
2
2 4
1
输出:
3
说明: 补种到2或4结果一样, 最多的连续胡杨棵树都是3.
示例2
输入:
10
3
2 4 7
1
输出:
6
说明: 种第7棵树, 最多连续胡杨树棵数位6 (5, 6, 7, 8, 9, 10)
题解
Python
def main():
# 先读取输入值
# 种树的数量
total = int(input())
# 树树的数量
dead_count = int(input())
# 死树的位置
dead_list = list(map(int, input().split()))
# 补种的数量 k
k = int(input())
assert len(dead_list) == dead_count
# 树的生死状态
# 0 表示存活, 1 表示不存活
states = [0] * total
# 更新树的状态
# 注意这里是从1开始计数的, 要转换成从0开始计数
for dead in dead_list:
states[dead - 1] = 1
# 双指针法
left = 0
right = 0
# 最大连续存活的树的数量
max_alive = 0
# 窗口左侧边界经过的死树的数量
dead_left = 0
# 窗口右侧边界经过的死树的数量
dead_right = 0
# 遍历所有的树
while right < total:
# 更新窗口右侧经过的死树的数量
dead_right += states[right]
# 如果窗口内死树的数量比能补种的数量多, 就将窗口左侧往右移
while dead_right - dead_left > k:
dead_left += states[left]
left += 1
# 更新最大连续活着的树的数量
max_alive = max(max_alive, right - left + 1)
right += 1
print(max_alive)
if __name__ == "__main__":
main()
最小的调整次数/特异性双端队列
题目描述
有一个特异性的双端队列, 该队列可以从头部或尾部添加数据, 但是只能从头部移出数据.
小A依次执行2n个指令往队列中添加数据和移出数据. 其中n个指令是添加数据 (可能从头部添加, 也可能从尾部添加), 依次添加1到n, n个指令是移出数据.
现在要求移除数据的顺序为1到n.
为了满足最后输出的要求, 小A可以在任何时候调整队列中数据的顺序.
请问 小A 最少需要调整几次才能够满足移除数据的顺序正好是1到n.
输入描述
- 第一行一个数据n, 表示数据的范围
- 接下来的2n行, 其中有n行为添加数据, 指令为:
- head add x表示从头部添加数据 x
- tail add x 表示从尾部添加数据x
- 另外 n 行为移出数据指令, 指令为 remove 的形式, 表示移出1个数据
- 1 ≤ n ≤ 3 * 10^5
- 所有的数据均合法
输出描述
一个整数, 表示 小A 要调整的最小次数.
示例1
输入:
5
head add 1
tail add 2
remove
head add 3
tail add 4
head add 5
remove
remove
remove
remove
输出:
1
题解
Python
from collections import deque
def main():
# 读取输入, number 是整数n, 对队列的操作次数是 number * 2
number = int(input())
# 创建双端队列
queue = deque()
# 是否依照顺序删除
in_order = True
# 调整操作的次数
adjust_times = 0
# 遍历所有的整数, 依次检查对它的操作
for i in range(number * 2):
# 解析每次输入
parts = input().split()
op = parts[0]
if op == "head":
# 如果此时队列不为空, 说明这个插入导致了无序
if len(queue) > 0:
in_order = False
# 从头部插入整数
queue.appendleft(int(parts[2]))
elif op == "tail":
# 从尾部插入整数
queue.append(int(parts[2]))
elif op == "remove":
# 如果队列为空, 忽略它
if len(queue) == 0:
continue
# 如果不按顺序插入, 则需要调整一次
if not in_order:
adjust_times += 1
in_order = True
# 从头部移除整数
queue.popleft()
else:
# 无效输入
pass
# 输出结果
print(adjust_times)
if __name__ == "__main__":
main()
查找充电设备组合
题目描述
某个充电站, 可提供 n 个充电设备, 每个充电设备均有对应的输出功率.
任意个充电设备组合的输出功率总和, 均构成功率集合 P 的 1 个元素.
功率集合 P 的最优元素, 表示最接近充电站最大输出功率 p_max 的元素.
输入描述
输入为 3 行:
- 第 1 行为充电设备个数 n
- 第 2 行为每个充电设备的输出功率
- 第 3 行为充电站最大输出功率 p_max
备注:
- 充电设备个数 n > 0
- 最优元素必须小于或等于充电站最大输出功率 p_max
输出描述
功率集合 P 的最优元素.
示例1
输入:
4
50 20 20 60
90
输出:
90
示例2
输入:
2
50 40
30
输出:
0
示例3
输入:
3
1 2 3
5
输出:
5
题解
Python
def main():
# 读取输入
# 机器数量
num_machines = int(input())
assert 0 < num_machines
# 每台机器的输出功率
machine_powers = list(map(int, input().split()))
assert len(machine_powers) == num_machines
# 最大输出功率
max_power = int(input())
# 背包问题, DP
# 思路一: 暴力法, 时间复杂度 O(2^n)
target_power = 0
def recursive(machine_index, power_sum):
# 所有机器都访问完了
if machine_index >= num_machines:
diff = max_power - power_sum
nonlocal target_power
if 0 <= diff < (max_power - target_power):
target_power = power_sum
return
# 递归调用
recursive(machine_index + 1, power_sum + machine_powers[machine_index])
recursive(machine_index + 1, power_sum)
recursive(0, 0)
# 打印结果
print(target_power)
if __name__ == "__main__":
main()
智能成绩表
题目描述
小明来到学校当老师, 需要将学生按考试总分或单科分数进行排名, 你能帮帮他吗?
输入描述
- 第 1 行输入两个整数, 学生人数 n 和科目数量 m
- 0 < n < 100
- 0 < m < 10
- 第 2 行输入 m 个科目名称, 彼此之间用空格隔开
- 科目名称只由英文字母构成, 单个长度不超过10个字符
- 科目的出现顺序和后续输入的学生成绩一一对应
- 不会出现重复的科目名称
- 第 3 行开始的 n 行, 每行包含一个学生的姓名和该生 m 个科目的成绩 (空格隔开)
- 学生不会重名
- 学生姓名只由英文字母构成, 长度不超过10个字符
- 成绩是0~100的整数, 依次对应第2行种输入的科目
- 第n+2行, 输入用作排名的科目名称. 若科目不存在, 则按总分进行排序
输出描述
输出一行, 按成绩排序后的学生名字, 空格隔开. 成绩相同的按照学生姓名字典顺序排序.
示例1
输入:
3 2
yuwen shuxue
fangfang 95 90
xiaohua 88 98
minmin 100 82
shuxue
输出:
xiaohua fangfang minmin
示例2
输入:
3 2
yuwen shuxue
fangfang 95 90
xiaohua 88 95
minmin 90 95
zongfen
输出:
fangfang minmin xiaohua
题解
Python
def main():
# 读取输入
parts = input().split()
num_students = int(parts[0])
num_courses = int(parts[1])
course_list = list(input().split())
students = []
for i in range(num_students):
part = input().split()
name = part[0]
individual_scores = list(map(int, part[1:]))
assert len(individual_scores) == num_courses
# 计算该学生的总分
total_score = sum(individual_scores)
students.append((name, individual_scores, total_score))
sorted_by_course = input()
# 分数要按照降序的方式来排序
if sorted_by_course != "zongfen":
# 如果指定的科目, 先找到它在分数列表中的索引位置, 再依此排序
course_index = course_list.index(sorted_by_course)
students.sort(key = lambda student: student[1][course_index], reverse = True)
else:
# 如果没有指定排序的科目, 就依总分来排序
students.sort(key = lambda student: student[2], reverse = True)
# 最后打印学生的名字
print(" ".join(student[0] for student in students))
if __name__ == "__main__":
main()
虚拟理财游戏
题目描述
在一款虚拟游戏中生活, 你必须进行投资以增强在虚拟游戏中的资产以免被淘汰出局.
现有一家Bank, 它提供有若干理财产品 m 个, 风险及投资回报不同, 你有 N (元) 进行投资, 能接收的总风险值为X.
你要在可接受范围内选择最优的投资方式获得最大回报.
备注:
- 在虚拟游戏中, 每项投资风险值相加为总风险值
- 在虚拟游戏中, 最多只能投资2个理财产品
- 在虚拟游戏中, 最小单位为整数, 不能拆分为小数
- 投资额*回报率=投资回报
输入描述
- 第一行:
- 产品数 (取值范围[1,20])
- 总投资额 (整数, 取值范围[1, 10000])
- 可接受的总风险 (整数, 取值范围[1,200])
- 第二行: 产品投资回报率序列, 输入为整数, 取值范围[1,60]
- 第三行: 产品风险值序列, 输入为整数, 取值范围[1, 100]
- 第四行: 最大投资额度序列, 输入为整数, 取值范围[1, 10000]
输出描述
每个产品的投资额序列.
示例1
输入:
5 100 10
10 20 30 40 50
3 4 5 6 10
20 30 20 40 30
输出:
0 30 0 40 0
说明: 投资第二项30个单位, 第四项40个单位, 总的投资风险为两项相加为4+6=10.
题解
手机App防沉迷系统
题目描述
智能手机方便了我们生活的同时, 也侵占了我们不少的时间. 手机App防沉迷系统能够让我们每天合理地规划手机App使用时间, 在正确的时间做正确的事.
它的大概原理是这样的:
- 在一天24小时内, 可以注册每个App的允许使用时段
- 一个时间段只能使用一个App
- App有优先级, 数值越高, 优先级越高. 注册使用时段时, 如果高优先级的App时间和低优先级的时段有冲突, 则系统会自动注销低优先级的时段, 如果App的优先级相同, 则后添加的App不能注册
请编程实现, 根据输入数据注册App, 并根据输入的时间点, 返回时间点使用的App名称, 如果该时间点没有注册任何App, 请返回字符串
NA
.
输入描述
- 第一行表示注册的App数量 N(N ≤ 100)
- 第二部分包括 N 行, 每行表示一条App注册数据
- 最后一行输入一个时间点, 程序即返回该时间点使用的App
2
App1 1 09:00 10:00
App2 2 11:00 11:30
09:30
数据说明如下:
- N行注册数据以空格分隔, 四项数依次表示: App名称、优先级、起始时间、结束时间
- 优先级1~5, 数字越大, 优先级越高
- 时间格式 HH:MM, 小时和分钟都是两位, 不足两位前面补0
- 起始时间需小于结束时间, 否则注册不上
- 注册信息中的时间段包含起始时间点, 不包含结束时间点
输出描述
输出一个字符串, 表示App名称, 或NA表示空闲时间.
示例1
输入:
1
App1 1 09:00 10:00
09:30
输出:
App1
示例2
输入:
2
App1 1 09:00 10:00
App2 2 09:10 09:30
09:20
输出:
App2
示例3
输入:
2
App1 1 09:00 10:00
App2 2 09:10 09:30
09:50
输出:
NA
题解
Python
def main():
# 先读取输入
# 然后构造列表, 将每个app都注册进去
# 最后查找给定时间点内的app
num_apps = int(input())
input_apps = []
for i in range(num_apps):
parts = input().split()
assert len(parts) == 4
# 将时间点转换成整数
start_time = int(parts[2].replace(":", ""))
end_time = int(parts[3].replace(":", ""))
app = (parts[0], int(parts[1]), start_time, end_time)
input_apps.append(app)
# 读取查询的时间点
searched_time = int(input().replace(":", ""))
NAME = 0
PRIORITY = 1
START_TIME = 2
END_TIME = 3
# 注册app
validated_apps = []
for app in input_apps:
skip_app = False
for i in range(len(validated_apps)):
valid_app = validated_apps[i]
# 检查时间段是否有冲突
if valid_app[START_TIME] >= app[END_TIME] or valid_app[END_TIME] <= app[START_TIME]:
continue
# 检查优先级, 如果待注册的app优先级不高于已注册的app, 就不注册它
if valid_app[PRIORITY] >= app[PRIORITY]:
skip_app = True
else:
# 将已注册的app清除, 因为它优先级更低
validated_apps.pop(i)
break
if not skip_app:
validated_apps.append(app)
for app in validated_apps:
# 查询的时间点在该 app 的服务时间段内
if app[START_TIME] <= searched_time < app[END_TIME]:
print(app[0])
return
print("NA")
if __name__ == "__main__":
main()
构成正方形的数量
题目描述
输入N个互不相同的二维整数坐标, 求这N个坐标可以构成的正方形数量. 内积为零的的两个向量垂直.
输入描述
第一行输入为N, N代表坐标数量, N为正整数. N <= 100
之后的 K 行输入为坐标x y以空格分隔, x, y为整数, -10 <= x, y <= 10.
输出描述
输出可以构成的正方形数量.
示例1
输入:
3
1 3
2 4
3 1
输出:
0
示例2
输入:
4
0 0
1 2
3 1
2 -1
输出:
1
题解
Python
def main():
# 读取输入
# 四个点构成正方形的条件:
# 1. 对角线成90度, 即点积为0
# 2. 对角线长度相等
# 两层遍历所有的坐标点, 就构造出一个对角线
# 然后计算该对角线成90的另外的几个对角线, 是否也存在坐标点中
num_points = int(input())
assert 0 < num_points <= 100
points = []
for i in range(num_points):
# 存储所有的坐标点
point = tuple(map(int, input().split()))
assert -10 <= point[0] <= 10
assert -10 <= point[1] <= 10
points.append(point)
num_squares = 0
points_set = set(points)
# 遍历所有的点, 检查能否构成正方形
for i in range(num_points):
x1, y1 = points[i]
for j in range(i + 1, num_points):
x2, y2 = points[j]
# 计算两个对角点
x3, y3 = x1 - (y1 - y2), y1 + (x1 - x2)
x4, y4 = x2 - (y1 - y2), y2 + (x1 - x2)
p3 = (x3, y3)
p4 = (x4, y4)
if p3 in points_set and p4 in points_set:
num_squares += 1
# 计算另外两个对角点
x5, y5 = x1 + (y1 - y2), y1 - (x1 - x2)
x6, y6 = x2 + (y1 - y2), y2 - (x1 - x2)
p5 = (x5, y5)
p6 = (x6, y6)
if p5 in points_set and p6 in points_set:
num_squares += 1
# 因为对角线计算了4次, 我们来去重
print(num_squares)
num_squares = num_squares // 4
print(num_squares)
if __name__ == "__main__":
main()
单词接龙
题目描述
单词接龙的规则是:
- 可用于接龙的单词首字母必须要前一个单词的尾字母相同
- 当存在多个首字母相同的单词时, 取长度最长的单词, 如果长度也相等, 则取字典序最小的单词
- 已经参与接龙的单词不能重复使用
现给定一组全部由小写字母组成单词数组, 并指定其中的一个单词作为起始单词, 进行单词接龙, 请输出最长的单词串, 单词串是单词拼接而成, 中间没有空格.
输入描述
- 输入的第一行为一个非负整数, 表示起始单词在数组中的索引K,
0 <= K < N
- 输入的第二行为一个非负整数, 表示单词的个数N
- 接下来的N行, 分别表示单词数组中的单词
备注:
- 单词个数N的取值范围为
[1, 20]
- 单个单词的长度的取值范围为
[1, 30]
输出描述
输出一个字符串, 表示最终拼接的单词串.
示例1
输入:
0
6
word
dd
da
dc
dword
d
输出:
worddwordda
示例2
输入:
4
6
word
dd
da
dc
dword
d
输出:
dwordda
题解
Python
def main():
# 读取输入
first_word_index = int(input())
word_count = int(input())
assert 0 <= first_word_index < word_count
words = []
for i in range(word_count):
words.append(input())
s = words[first_word_index]
# 将第一个单词从字典中去除, 然后重整字典
words.pop(first_word_index)
# 开始接龙, 如果字典已经空了, 就终止
while words:
next_char = s[-1]
last_word_index = -1
last_word = ""
# 遍历字典, 找到以 next_char 开头的单词
for i in range(len(words)):
word = words[i]
# 将当前词更新为 last_word 的条件有:
# - 当前词的长度比上个词长
# - 或者当前词的字典序小于上个词
if word.startswith(next_char):
if len(word) > len(last_word) or (len(word) == len(last_word) and word < last_word):
last_word = word
last_word_index = i
# 没有找到合适的单词, 终止循环
if last_word_index == -1:
break
# 接龙, 并将该单词从字典中移除
s += last_word
words.pop(last_word_index)
print(s)
if __name__ == "__main__":
main()
Rust
use std::io::{stdin, BufRead}; fn main() { // 读取输入 let mut line = String::new(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let first_word_index: usize = line.trim().parse().unwrap(); line.clear(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let word_count: usize = line.trim().parse().unwrap(); assert!(first_word_index < word_count); let mut words: Vec<String> = Vec::with_capacity(word_count); for _i in 0..word_count { line.clear(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); words.push(line.trim().to_owned()); } // 将第一个单词从字典中去除, 然后重整字典 let mut ans: String = words.remove(first_word_index).to_owned(); // 开始接龙, 如果字典已经空了, 就终止 while !words.is_empty() { let next_char: char = ans.chars().last().unwrap(); let mut last_word_index = usize::MAX; let mut last_word = String::new(); // 遍历字典, 找到以 next_char 开头的单词 for (index, word) in words.iter().enumerate() { // 将当前词更新为 last_word 的条件有: // - 当前词的长度比上个词长 // - 或者当前词的字典序小于上个词 if word.starts_with(next_char) && (word.len() > last_word.len() || (word.len() == last_word.len() && *word < last_word)) { last_word = word.to_string(); last_word_index = index; } } // 没有找到合适的单词, 终止循环 if last_word_index == usize::MAX { break; } // 接龙, 并将该单词从字典中移除 ans.push_str(&last_word); words.remove(last_word_index); } // 打印结果 println!("{ans}"); }
跳房子I
题目描述
跳房子, 也叫跳飞机, 是一种世界性的儿童游戏.
游戏参与者需要分多个回合按顺序跳到第1格直到房子的最后一格.
跳房子的过程中, 可以向前跳, 也可以向后跳.
假设房子的总格数是count, 小红每回合可能连续跳的步教都放在数组steps中, 请问数组中是否有一种步数的组合, 可以让小红两个回合跳到量后一格?
如果有, 请输出索引和最小的步数组合.
注意:
- 数组中的步数可以重复, 但数组中的元素不能重复使用
- 提供的数据保证存在满足题目要求的组合, 且索引和最小的步数组合是唯一的
输入描述
- 第一行输入为每回合可能连续跳的步数, 它是int整数数组类型.
- 第二行输入为房子总格数count, 它是int整数类型.
备注:
- count ≤ 1000
- 0 ≤ steps.length ≤ 5000
- -100000000 ≤ steps ≤ 100000000
输出描述
返回索引和最小的满足要求的步数组合 (顺序保持steps中原有顺序).
示例1
输入:
[1,4,5,2,2]
7
输出:
[5, 2]
示例2
输入:
[-1,2,4,9,6]
8
输出:
[-1, 9]
题解
Python
def main():
# 读取输入
steps_str = input()
steps = list(map(int, steps_str[1:-1].split(",")))
assert 0 <= len(steps) <= 5000
count = int(input())
assert 0 < count <= 1000
# 所谓的两步, 就是两数之和等于 count
# 直接遍历, 或者先生成一个字典, 加快搜索
min_index = 10 ** 9
ans = []
for i in range(len(steps) - 1):
for j in range(i + 1, len(steps)):
# 两数之和等于 count
# 并且两数索引之和更小
if steps[i] + steps[j] == count and i + j < min_index:
min_index = min(min_index, i + j)
ans = [steps[i], steps[j]]
# 忽略掉后面的步骤
if i + j > min_index:
break
# 忽略掉后面的步骤
if i + i > min_index:
break
# 打印结果
print("[%d, %d]" % (ans[0], ans[1]))
if __name__ == "__main__":
main()
第k个排列
题目描述
给定参数n, 从1到n会有n个整数: 1,2,3,…,n,这n个数字共有n!种排列.
按大小顺序升序列出所有排列的情况, 并一一标记,
当n=3时, 所有排列如下:
"123", "132" "213" "231" "312" "321"
给定n和k, 返回第k个排列.
输入描述
- 输入两行, 第一行为n, 第二行为k
- 给定n的范围是 [1,9], 给定k的范围是 [1,n!]
输出描述
输出排在第k位置的数字.
示例1
输入:
3
3
输出:
213
示例2
输入:
2
2
输出:
21
题解
Python
import itertools
def main():
def power(n: int) -> int:
assert n > 0
p = 1
for i in range(1, n + 1):
p *= i
return p
# 读取输入
# 生成所有的排列
# 找到第k个排列
n = int(input())
assert 1 <= n <= 9
k = int(input())
power_n = power(n)
assert 1 <= k <= power_n
# 生成所有的数字
nums = tuple(i for i in range(1, n + 1))
# 生成排列的迭代器
it = itertools.permutations(nums)
# 注意, k是从1开始计数的
for i in range(k):
ans = next(it)
# 打印结果
s = "".join(map(str, ans))
print(s)
if __name__ == "__main__":
main()
C++
#include <cassert>
#include <algorithm>
#include <iostream>
#include <vector>
void solution() {
int n = 0;
std::cin >> n;
assert(n > 0);
int k = 0;
std::cin >> k;
assert(k > 0);
std::string line;
for (int i = 1; i <= n; ++i) {
line += i + '0';
}
int count = 1;
while (count < k && std::next_permutation(line.begin(), line.end())) {
count += 1;
}
std::cout << line << std::endl;
}
int main() {
solution();
return 0;
}
喊7的次数重排
题目描述
喊7是一个传统的聚会游戏, N个人围成一圈, 按顺时针从1到N编号.
编号为1的人从1开始喊数, 下一个人喊的数字为上一个人的数字加1, 但是当将要喊出来的数字是7的倍数或者数字本身含有7的话, 不能把这个数字直接喊出来, 而是要喊"过".
假定玩这个游戏的N个人都没有失误地在正确的时机喊了"过", 当喊到数字K时, 可以统计每个人喊"过"的次数.
现给定一个长度为N的数组, 存储了打乱顺序的每个人喊"过"的次数, 请把它还原成正确的顺序, 即数组的第i个元素存储编号i的人喊" 过"的次数.
输入描述
输入为一行, 为空格分隔的喊"过"的次数, 注意K并不提供, K不超过200, 而数字的个数即为N.
输出描述
输出为一行, 为顺序正确的喊"过"的次数, 也由空格分隔.
示例1
输入:
0 1 0
输出:
1 0 0
说明: 一共只有一次喊"过", 那只会发生在需要喊7时, 按顺序, 编号为1的人会遇到7, 故输出1 0 0
.
示例2
输入:
0 1 2 0 0
输出:
0 2 0 1 0
说明: 一共有三次喊"过", 发生在 7 14 17
, 按顺序, 编号为2的人会遇到 7 17
, 编号为4的人会遇到14, 故输出 0 2 0 1 0
.
题解
Python
def main():
# 读取输入
# 确定有人数 n
# 确定总共喊了多少声
# 基于此, 就可以确定喊的顺序
pass_list = list(map(int, input().split()))
# 得到人数
num_people = len(pass_list)
assert 0 < num_people
# 喊过的总次数
total_passes = sum(pass_list)
# 喊过的条件是:
# 1. 当前数字是7的倍数
# 2. 当前数字中包含7
# 每个人真正喊过的次数
real_pass_list = [0] * num_people
# 当前的喊的数字 k, 注意数字是从1开始
current_num = 1
# 当前喊数的人在队列中的位置, 从0开始计数
current_person = 0
# 一直循环, 直到喊过的次数用完了
while total_passes > 0:
if current_num % 7 == 0 or "7" in str(current_num):
# 这个人要喊过
real_pass_list[current_person] += 1
total_passes -= 1
current_num += 1
current_person = (current_person + 1) % num_people
# 打印结果
print(" ".join(map(str, real_pass_list)))
if __name__ == "__main__":
main()
Rust
use std::io::{stdin, BufRead}; fn number_digits_contains(mut num: u32, digit: u8) -> bool { while num > 0 { let d = (num % 10) as u8; if d == digit { return true; } num /= 10; } false } fn solution() { // 读取输入 // 确定有人数 n // 确定总共喊了多少声 // 基于此, 就可以确定喊的顺序 let mut line = String::new(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let pass_list: Vec<u32> = line .trim() .split_ascii_whitespace() .map(|s| s.parse().unwrap()) .collect(); // 得到人数 let num_people = pass_list.len(); assert!(0 < num_people); // 喊过的总次数 let mut total_passes: u32 = pass_list.iter().sum(); // 喊过的条件是: // 1. 当前数字是7的倍数 // 2. 当前数字中包含7 // 每个人真正喊过的次数 let mut real_pass_list: Vec<usize> = vec![0; num_people]; // 当前的喊的数字 k, 注意数字是从1开始 let mut current_num = 1; // 当前喊数的人在队列中的位置, 从0开始计数 let mut current_person = 0; // 一直循环, 直到喊过的次数用完了 while total_passes > 0 { if current_num % 7 == 0 && number_digits_contains(current_num, 7) { // 这个人要喊过 real_pass_list[current_person] += 1; total_passes -= 1; } current_num += 1; current_person = (current_person + 1) % num_people; } // 打印结果 let s = real_pass_list .into_iter() .map(|x| x.to_string()) .collect::<Vec<String>>() .join(" "); println!("{s}"); } fn main() { solution(); }
英文输入法
题目描述
主管期望你来实现英文输入法单词联想功能.
需求如下:
- 依据用户输入的单词前缀, 从已输入的英文语句中联想出用户想输入的单词, 按字典序输出联想到的单词序列
- 如果联想不到, 请输出用户输入的单词前缀
注意:
- 英文单词联想时, 区分大小写
- 缩略形式如
don’t
, 判定为两个单词,don
和t
- 输出的单词序列, 不能有重复单词, 且只能是英文单词, 不能有标点符号
输入描述
输入为两行:
- 首行输入一段由英文单词word和标点符号组成的语句str
- 接下来一行为一个英文单词前缀pre.
- 0 < word.length() <= 20
- 0 < str.length <= 10000
- 0 < pre <= 20
输出描述
输出符合要求的单词序列或单词前缀, 存在多个时, 单词之间以单个空格分割.
示例1
输入:
I love you
He
输出:
He
示例2
输入:
The furthest distance in the world, Is not between life and death, But when I stand in front of you, Yet you don’t know that I love you.
f
输出:
front furthest
题解
Python
import string
def main():
# 首先解析出所有的单词, 忽略掉空格以及标点等, 并生成一个集合, 以备查询
# 1. 区分大小写
# 2. 如果有'的话, 会被作为两个单词
#
# 然后将前缀字符串在集合中查找, 找到以此开头的;
# 当然, 如果考虑速度的话, 可以将单词存放列表, 并排序, 这样就可以用二分法;
# 或者生成一个字典树 trie tree, 查找前缀的速度会更快.
sentence = input()
prefix = input()
# 使用转换表, 将所有的标点都转换成空格,
# 然后以空格为分隔, 得到所有的单词
sentence = sentence.translate(str.maketrans(string.punctuation, " " * len(string.punctuation)))
words = set(sentence.split())
#print("dict:", words)
ans = []
for word in words:
if word.startswith(prefix):
ans.append(word)
ans.sort()
if ans:
print(" ".join(ans))
else:
print(prefix)
if __name__ == "__main__":
main()
高矮个子排队
题目描述
现在有一队小朋友, 他们高矮不同, 我们以正整数数组表示这一队小朋友的身高, 如数组{5,3,1,2,3}.
我们现在希望小朋友排队, 以高矮高矮
顺序排列, 每一个高
位置的小朋友要比相邻的位置高或者相等;
每一个矮
位置的小朋友要比相邻的位置矮或者相等
要求小朋友们移动的距离和最小, 第一个从“高”位开始排, 输出最小移动距离即可.
例如, 在示范小队{5,3,1,2,3}中, {5, 1, 3, 2, 3}是排序结果.
{5, 2, 3, 1, 3} 虽然也满足高矮高矮
顺序排列, 但小朋友们的移动距离大, 所以不是最优结果.
移动距离的定义如下所示:
第二位小朋友移到第三位小朋友后面, 移动距离为1, 若移动到第四位小朋友后面, 移动距离为2.
输入描述
排序前的小朋友, 以英文空格的正整数:
4 3 5 7 8
注: 小朋友<100个
输出描述
排序后的小朋友, 以英文空格分割的正整数: 4 3 7 5 8
备注: 4 (高) 3 (矮) 7 (高) 5 (矮) 8 (高) , 输出结果为最小移动距离, 只有5和7交换了位置, 移动距离都是1.
示例1
输入:
4 1 3 5 2
输出:
4 1 5 2 3
示例2
输入:
1 1 1 1 1
输出:
1 1 1 1 1
示例3
输入:
xxx
输出:
[ ]
示例4
输入:
4 3 5 7 8
输出:
4 3 7 5 8
示例5
输入:
5 3 1 2 3
输出:
5 1 3 2 3
题解
Python
def main():
try:
heights = list(map(int, input().split()))
except ValueError:
# Invalid input
print("[]")
return
i = 0
j = 1
while j < len(heights):
# 交换相邻小朋友的条件:
# - 相邻的两个小朋友身高不相同
# - 如果 i 是偶数, 并且 i 位小朋友的身高小于右侧 j 位的身高
# - 如果 i 是奇数, 并且 i 位小朋友的身高大于右侧 j 位的身高
if heights[i] != heights[j] and ((heights[i] > heights[j]) != (i % 2 == 0)):
heights[i], heights[j] = heights[j], heights[i]
i += 1
j += 1
print(" ".join(map(str, heights)))
if __name__ == "__main__":
main()
考勤信息
题目描述
公司用一个字符串来表示员工的出勤信息:
- absent: 缺勤
- late: 迟到
- leaveearly: 早退
- present: 正常上班
现需根据员工出勤信息, 判断本次是否能获得出勤奖, 能获得出勤奖的条件如下:
- 缺勤不超过一次
- 没有连续的迟到/早退
- 任意连续7次考勤, 缺勤/迟到/早退不超过3次
输入描述
用户的考勤数据字符串:
- 记录条数 >= 1
- 输入字符串长度 < 10000
- 不存在非法输入
如:
2
present
present absent present present leaveearly present absent
输出描述
根据考勤数据字符串, 如果能得到考勤奖, 输出true
; 否则输出false
,
对于输入示例的结果应为:
true false
示例1
输入:
2
present
present present
输出:
true true
示例2
输入:
2
present
present absent present present leaveearly present absent
输出:
true false
题解
Python
import sys
def main():
def to_state(s: str) -> int:
if s == "absent":
return ABSENT
elif s == "late":
return LATE
elif s == "leaveearly":
return LEAVE_EARLY
elif s == "present":
return PRESENT
else:
assert False, "Invalid state"
ABSENT = 0
LATE = 1
LEAVE_EARLY = 2
PRESENT = 3
# 先解析出所有的出勤记录
num_person = int(input())
person_presents = []
for line in sys.stdin.readlines():
person = list(map(to_state, line.split()))
person_presents.append(person)
assert num_person == len(person_presents)
# 然后基于三条规则, 过滤是否都成立
# 如果有一条不成立, 就返回 false
# 否则返回 true
person_awards = []
for person_present in person_presents:
award = True
# 缺勤
if person_present.count(ABSENT) > 1:
award = False
# 没有连续的迟到/早退
for i in range(len(person_present) - 1):
if person_present[i] in (LATE, LEAVE_EARLY) and person_present[i + 1] in (LATE, LEAVE_EARLY):
award = False
break
# 连续7次考勤, 迟到/早退/缺勤不超过3次
if len(person_present) > 7:
# 如果多于7次考勤, 就用滑动窗口的方式遍历所有考勤
for i in range(len(person_present) - 7):
if person_present[i:i+7].count(PRESENT) < 4:
award = False
break
else:
# 否则就只计算所有考勤
if person_present.count(ABSENT) + person_present.count(LATE) + person_present.count(LEAVE_EARLY) > 3:
award = False
person_awards.append(award)
# 输出结果
print(" ".join("true" if award else "false" for award in person_awards))
if __name__ == "__main__":
main()
找终点
题目描述
给定一个正整数数组, 设为nums, 最大为100个成员, 求从第一个成员开始, 正好走到数组最后一个成员, 所使用的最少步骤数.
要求:
- 第一步必须从第一元素开始, 且1<=第一步的步长<len/2; (len为数组的长度, 需要自行解析)
- 从第二步开始, 只能以所在成员的数字走相应的步数, 不能多也不能少, 如果目标不可达返回
-1
, 只输出最少的步骤数量 - 只能向数组的尾部走, 不能往回走
输入描述
由正整数组成的数组, 以空格分隔, 数组长度小于100, 请自行解析数据数量.
输出描述
正整数, 表示最少的步数, 如果不存在输出 -1
.
示例1
输入:
7 5 9 4 2 6 8 3 5 4 3 9
输出:
2
说明:
- 第一步: 第一个可选步长选择2, 从第一个成员7开始走2步, 到达9
- 第二步: 从9开始, 经过自身数字9对应的9个成员到最后
示例2
输入:
1 2 3 7 1 5 9 3 2 1
输出:
-1
题解
Python
def main():
def get_steps(first_step):
print("first step:", first_step)
pos = 0 + first_step
count = 0
last_pos = num_len - 1
while pos < last_pos :
print(" pos:", pos, " +step:", nums[pos])
pos += nums[pos]
count += 1
if pos == last_pos:
print(" got end pos:", last_pos)
return count
else:
return None
# 先解析每个位置对应的步数
nums = list(map(int, input().split()))
num_len = len(nums)
if num_len % 2 == 0:
max_first_step = num_len // 2
else:
max_first_step = (num_len - 1) // 2 + 1
# Brute force
# 唯一变化的就是第一步的步长, 我们遍历它所有可能的步长
all_steps = []
for first_step in range(1, max_first_step):
num_steps = get_steps(first_step)
if num_steps:
all_steps.append(num_steps)
if not all_steps:
print(-1)
return
# 对所有步长进行排序, 然后找到最小的步数
all_steps.sort()
print(all_steps[0])
if __name__ == "__main__":
main()
数组拼接
题目描述
现在有多组整数数组, 需要将它们合并成一个新的数组.
合并规则, 从每个数组里按顺序取出固定长度的内容合并到新的数组中, 取完的内容会删除掉, 如果该行不足固定长度或者已经为空, 则直接取出剩余部分的内容放到新的数组中, 继续下一行.
输入描述
- 第一行是每次读取的固定长度, 0<长度<10
- 第二行是整数数组的数目, 0<数目<1000
- 第3-n行是需要合并的数组, 不同的数组用回车换行分隔, 数组内部用逗号分隔, 最大不超过100个元素
输出描述
输出一个新的数组, 用逗号分隔.
示例1
输入:
3
2
2,5,6,7,9,5,7
1,7,4,3,4
输出:
2,5,6,1,7,4,7,9,5,3,4,7
说明:
- 获得长度3和数组数目2
- 先遍历第一行, 获得2,5,6
- 再遍历第二行, 获得1,7,4
- 再循环回到第一行, 获得7,9,5
- 再遍历第二行, 获得3,4
- 再回到第一行, 获得7, 按顺序拼接成最终结果
示例2
输入:
4
3
1,2,3,4,5,6
1,2,3
1,2,3,4
输出:
1,2,3,4,1,2,3,1,2,3,4,5,6
题解
Python
import sys
def main():
# 解析目标数组的长度 k
k = int(input())
num_arrays = int(input())
# 解析所有的数组
all_arrays = []
for line in sys.stdin.readlines():
all_arrays.append(list(map(int, line.split(","))))
#print("all_arrays:", all_arrays)
assert len(all_arrays) == num_arrays
# 依次遍历所有数组, 取出 k 个元素
# 如果不足k 个元素, 就取出剩下的所有元素, 然后与下面一个数组拼够 k 个元素
# 直到所有数组里的元素都被取出
i = 0
ans = []
nums_taken = k
while True:
current_array = all_arrays[i]
if len(current_array) > nums_taken:
# 取出足够的元素
ans.extend(current_array[:nums_taken])
all_arrays[i] = current_array[nums_taken:]
# 去下一个数组
i = (i + 1) % len(all_arrays)
nums_taken = k
else:
# 还有几个元素等下一轮获取
nums_taken -= len(current_array)
# 取出所有元素
ans.extend(current_array[:])
# 将当前数组移除
all_arrays.pop(i)
if all_arrays:
i = i % len(all_arrays)
else:
break
for num in ans[:-1]:
print(num, ",", sep="", end="")
if ans:
print(ans[-1])
if __name__ == "__main__":
main()
整数对最小和
题目描述
给定两个整数数组array1, array2, 数组元素按升序排列.
假设从array1、array2中分别取出一个元素可构成一对元素, 现在需要取出k对元素, 并对取出的所有元素求和, 计算和的最小值.
注意:
两对元素如果对应于array1, array2中的两个下标均相同, 则视为同一对元素.
输入描述
- 输入两行数组array1, array2, 每行首个数字为数组大小size (0 < size <= 100)
- 0 < array1[i] <= 1000
- 0 < array2[i] <= 1000
- 接下来一行为正整数k
- 0 < k <= array1.size() * array2.size()
输出描述
满足要求的最小和.
示例1
输入:
3 1 1 2
3 1 2 3
2
输出:
4
说明:
- 用例中, 需要取2对元素
- 取第一个数组第0个元素与第二个数组第0个元素组成1对元素[1,1]
- 取第一个数组第1个元素与第二个数组第0个元素组成1对元素[1,1]
- 求和为1+1+1+1=4, 为满足要求的最小和
题解
Python
import sys
def main():
# 提取 array1
part1 = input().split()
size1 = int(part1[0])
array1 = list(map(int, part1[1:]))
assert len(array1) == size1
assert 0 < size1 <= 100
# 提取 array2
part2 = input().split()
size2 = int(part2[0])
array2 = list(map(int, part2[1:]))
assert len(array2) == size2
assert 0 < size2 <= 100
# 提取整数 k
k = int(input())
# Brute force
# 取得所有的数对
all_pairs = []
for num1 in array1:
for num2 in array2:
all_pairs.append(num1 + num2)
# 以升序排序数对
all_pairs.sort()
# 取出前 k 对数对的和, 并计算其总和
ans = sum(all_pairs[:k])
print(ans)
if __name__ == "__main__":
main()
环中最长子串/字符成环找偶数O
题目描述
给你一个字符串 s, 字符串s首尾相连成一个环形, 请你在环中找出 o
字符出现了偶数次最长子字符串的长度.
输入描述
输入是一串小写字母组成的字符串
备注:
- 1 <= s.length <= 5 x 10^5
- s 只包含小写英文字母
输出描述
输出是一个整数.
示例1
输入:
alolobo
输出:
6
说明: 最长子字符串之一是 alolob
, 它包含 o
2个.
示例2
输入:
looxdolx
输出:
7
说明: 最长子字符串是 oxdolxl
, 由于是首尾连接在一起的, 所以最后一个 x
和开头的 l
是连接在一起的, 此字符串包含 2 个
o
.
示例3
输入:
bcbcbc
输出:
6
说明: 这个示例中, 字符串 bcbcbc
本身就是最长的, 因为 o
都出现了 0 次.
题解
找数字/找等值元素
题目描述
给一个二维数组 nums, 对于每一个元素 nums[i], 找出距离最近的且值相等的元素,
输出横纵坐标差值的绝对值之和, 如果没有等值元素, 则输出-1.
输入描述
- 输入第一行为二维数组的行
- 输入第二行为二维数组的列
- 输入的数字以空格隔开
备注:
- 针对数组 nums[i][j], 满足 0 < i ≤ 100, 0 < j ≤ 100
- 对于每个数字, 最多存在 100 个与其相等的数字
输出描述
数组形式返回所有坐标值.
示例1
输入:
3
5
0 3 5 4 2
2 5 7 8 3
2 5 4 2 4
输出:
[[-1, 4, 2, 3, 3], [1, 1, -1, -1, 4], [1, 1, 2, 3, 2]]
题解
光伏场地建设规划
题目描述
祖国西北部有一片大片荒地, 其中零星的分布着一些湖泊, 保护区, 矿区; 整体上常年光照良好, 但是也有一些地区光照不太好.
某电力公司希望在这里建设多个光伏电站, 生产清洁能源对每平方公里的土地进行了发电评估, 其中不能建设的区域发电量为0kw, 可以发电的区域根据光照, 地形等给出了每平方公里年发电量x千瓦. 我们希望能够找到其中集中的矩形区域建设电站, 能够获得良好的收益.
输入描述
- 第一行输入为调研的地区长, 宽, 以及准备建设的电站 (长宽相等, 为正方形) 的边长最低要求的发电量
- 之后每行为调研区域每平方公里的发电量
输出描述
输出为这样的区域有多少个.
示例1
输入:
2 5 2 6
1 3 4 5 8
2 3 6 7 1
输出:
4
说明:
- 输入: 调研的区域大小为长2宽5的矩形, 我们要建设的电站的边长为2, 建设电站最低发电量为6
- 输出: 长宽为2的正方形满足发电量大于等于6的区域有4个
示例2
输入:
5 1 6
1 3 4 5 8
2 3 6 7 1
输出:
3
题解
We are a team
题目描述
总共有 n 个人在机房, 每个人有一个标号(1<=标号<=n), 他们分成了多个团队, 需要你根据收到的 m 条消息判定指定的两个人是否在一个团队中, 具体的:
- 消息构成为 a b c, 整数 a, b 分别代表两个人的标号, 整数 c 代表指令
- c == 0 代表 a 和 b 在一个团队内
- c == 1 代表需要判定 a 和 b 的关系, 如果 a 和 b 是一个团队, 输出一行
we are a team
, 如果不是, 输出一行we are not a team
- c 为其他值, 或当前行 a 或 b 超出 1~n 的范围, 输出
da pian zi
输入描述
- 第一行包含两个整数 n, m(1<=n,m<100000), 分别表示有 n 个人和 m 条消息
- 随后的 m 行, 每行一条消息, 消息格式为 a b c(1<=a,b<=n,0<=c<=1)
输出描述
- c ==1, 根据 a 和 b 是否在一个团队中输出一行字符串, 在一个团队中输出
we are a team
, 不在一个团队中输出we are not a team
- c 为其他值, 或当前行 a 或 b 的标号小于 1 或者大于 n 时, 输出字符串
da pian zi
- 如果第一行 n 和 m 的值超出约定的范围时, 输出字符串
Null
示例1
输入:
5 7
1 2 0
4 5 0
2 3 0
1 2 1
2 3 1
4 5 1
1 5 1
输出:
We are a team
We are a team
We are a team
We are not a team
示例2
输入:
5 6
1 2 0
1 2 1
1 5 0
2 3 1
2 5 1
1 3 2
输出:
we are a team
we are not a team
we are a team
da pian zi
题解
生成哈夫曼树
题目描述
给定长度为 n nn 的无序的数字数组, 每个数字代表二叉树的叶子节点的权值, 数字数组的值均大于等于 1 11. 请完成一个函数, 根据输入的数字数组, 生成哈夫曼树, 并将哈夫曼树按照中序遍历输出.
为了保证输出的二叉树中序遍历结果统一, 增加以下限制:又树节点中, 左节点权值小于等于右节点权值, 根节点权值为左右节点权值之和. 当左右节点权值相同时, 左子树高度高度小于等于右子树.
注意: 所有用例保证有效, 并能生成哈夫曼树提醒:哈夫曼树又称最优二叉树, 是一种带权路径长度最短的一叉树.
所谓树的带权路径长度, 就是树中所有的叶结点的权值乘上其到根结点的路径长度 (若根结点为 0 00层, 叶结点到根结点的路径长度为叶结点的层数).
输入描述
例如: 由叶子节点 5 15 40 30 10 生成的最优二叉树如下图所示, 该树的最短带权路径长度为 40 * 1 + 30 * 2 +5 * 4 + 10 * 4 = 205
输出描述
输出一个哈夫曼的中序遍历数组, 数值间以空格分隔.
示例1
输入:
5
5 15 40 30 10
输出:
40 100 30 60 15 30 5 15 10
题解
内存资源分配
题目描述
有一个简易内存池, 内存按照大小粒度分类, 每个粒度有若干个可用内存资源, 用户会进行一系列内存申请, 需要按需分配内存池中的资源返回申请结果成功失败列表.
分配规则如下:
- 分配的内存要大于等于内存的申请量, 存在满足需求的内存就必须分配, 优先分配粒度小的, 但内存不能拆分使用
- 需要按申请顺序分配, 先申请的先分配, 有可用内存分配则申请结果为true
- 没有可用则返回false
注意: 不考虑内存释放.
输入描述
输入为两行字符串:
- 第一行为内存池资源列表, 包含内存粒度数据信息, 粒度数据间用逗号分割
- 一个粒度信息内用冒号分割, 冒号前为内存粒度大小, 冒号后为数量
- 资源列表不大于1024
- 每个粒度的数量不大于4096
- 第二行为申请列表, 申请的内存大小间用逗号分割
- 申请列表不大于100000
如:
64:2,128:1,32:4,1:128
50,36,64,128,127
输出描述
输出为内存池分配结果.
如 true,true,true,false,false.
示例1
输入:
64:2,128:1,32:4,1:128
50,36,64,128,127
输出:
true,true,true,false,false
题解
矩形相交的面积
题目描述
给出3组点坐标(x, y, w, h), -1000<x, y<1000, w,h为正整数.
(x, y, w, h)表示平面直角坐标系中的一个矩形:
- x, y为矩形左上角坐标点, w, h向右w, 向下h.
- (x, y, w, h)表示x轴(x, x+w)和y轴(y, y-h)围成的矩形区域
- (0, 0, 2, 2)表示 x轴(0, 2)和y 轴(0, -2)围成的矩形区域
- (3, 5, 4, 6)表示x轴(3, 7)和y轴(5, -1)围成的矩形区域
求3组坐标构成的矩形区域重合部分的面积.
输入描述
3行输入分别为3个矩形的位置, 分别代表 左上角x坐标
, 左上角y坐标
, 矩形宽
, 矩形高
-1000 <= x, y < 1000.
输出描述
输出3个矩形相交的面积, 不相交的输出0.
示例1
输入:
1 6 4 4
3 5 3 4
0 3 7 3
输出:
2
题解
水仙花数I
题目描述
所谓水仙花数, 是指一个n位的正整数, 其各位数字的n次方和等于该数本身.
例如153是水仙花数, 153是一个3位数, 并且153 = 1^3 + 5^3 + 3^3.
输入描述
- 第一行输入一个整数n, 表示一个n位的正整数. n在3到7之间, 包含3和7
- 第二行输入一个正整数m, 表示需要返回第m个水仙花数
输出描述
- 返回长度是n的第m个水仙花数, 个数从0开始编号
- 若m大于水仙花数的个数, 返回最后一个水仙花数和m的乘积
- 若输入不合法, 返回-1
示例1
输入:
3
0
输出:
153
说明: 153是第一个水仙花数
示例2
输入:
9
1
输出:
-1
说明: 9超出范围
题解
不等式是否满足约束并输出最大差
题目描述
给定一组不等式, 判断是否成立并输出不等式的最大差(输出浮点数的整数部分).
要求:
- 不等式系数为 double类型, 是一个二维数组
- 不等式的变量为 int类型, 是一维数组;
- 不等式的目标值为 double类型, 是一维数组
- 不等式约束为字符串数组, 只能是:“>”,“>=”,“<”,“<=”,“=”,
例如, 不等式组:
a11x1 + a12x2 + a13x3 + a14x4 + a15x5 <= b1;
a21x1 + a22x2 + a23x3 + a24x4 + a25x5 <= b2;
a31x1 + a32x2 + a33x3 + a34x4 + a35x5 <= b3;
最大差 = max{(a11x1+a12x2+a13x3+a14x4+a15x5-b1),(a21x1+a22x2+a23x3+a24x4+ a25x5-b2),(a31x1+a32x2+a33x3+a34x4+a35x5-b3)},
类型为整数(输出浮点数的整数部分).
输入描述
a11,a12,a13,a14,a15,a21,a22,a23,a24,a25, a31,a32,a33,a34,a35,x1,x2,x3,x4,x5,b1,b2,b3,<=,<=,<=
- 不等式组系数(double类型):
- a11,a12,a13,a14,a15
- a21,a22,a23,a24,a25
- a31,a32,a33,a34,a35
- 不等式变量(int类型): x1,x2,x3,x4,x5
- 不等式目标值(double类型): b1,b2,b3
- 不等式约束(字符串类型): <=,<=,<=
输出描述
true 或者 false, 最大差.
示例1
输入:
2.3,3,5.6,7.6;11,3,8.6,25,1;0.3,9,5.3,66,7.8;1,3,2,7,5;340,670,80.6;<=,<=,<=
输出:
false 458
示例2
输入:
2.36,3,6,7.1,6;1,30,8.6,2.5,21;0.3,69,5.3,6.6,7.8;1,13,2,17,5;340,67,300.6;<=,>=,<=
输出:
false 758
题解
字符统计及重排
题目描述
给出一个仅包含字母的字符串, 不包含空格, 统计字符串中各个字母(区分大小写)出现的次数,
并按照字母出现次数从大到小的顺序. 输出各个字母及其出现次数.
如果次数相同, 按照自然顺序进行排序, 且小写字母在大写字母之前.
输入描述
输入一行, 为一个仅包含字母的字符串.
输出描述
按照字母出现次数从大到小的顺序输出各个字母和字母次数, 用英文分号分隔, 注意末尾的分号.
字母和次数间用英文冒号分隔.
示例1
输入:
xyxyXX
输出:
x:2;y:2;X:2;
示例2
输入:
abababb
输出:
b:4;a:3;
说明: b的出现个数比a多,故b排在a之前
题解
Python
def solution1():
# 创建计数数组
# 读取输入, 并统计所有字母出现的次数
# 统计出现的最多次数
# 然后逆向遍历所有的次数, 并打印结果
chars = [0] * 256
for char in input().strip():
index = ord(char)
chars[index] += 1
max_count = max(chars)
out = []
for count in range(max_count, 0, -1):
# 遍历所有的小写字母
for char_index in range(ord('a'), ord('z') + 1):
if chars[char_index] == count:
out.append("{}:{}".format(chr(char_index), count))
# 再遍历所有的大写字母
for char_index in range(ord('A'), ord('Z') + 1):
if chars[char_index] == count:
out.append("{}:{}".format(chr(char_index), count))
# 打印结果
s = ";".join(out)
print(s)
def solution2():
# 读取输入
# 创建计数字典
# 统计所有字母出现的次数
# 然后转换成数组, 并对其进行排序
chars = dict()
for char in input().strip():
if char not in chars:
chars[char] = 0
chars[char] += 1
print(chars_list)
# 打印结果
s = ";".join(F"{char}:{count}" for (char, count) in chars_list)
print(s)
def main():
solution1()
if __name__ == "__main__":
main()
Rust
use std::io::{stdin, BufRead}; fn main() { // 创建计数数组 // 读取输入, 并统计所有字母出现的次数 // 统计出现的最多次数 // 然后逆向遍历所有的次数, 并打印结果 let mut chars = [0_usize; 256]; let mut line = String::new(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); for byte in line.trim().bytes() { let index: usize = byte as usize; chars[index] += 1; } let max_count: usize = chars.iter().max().copied().unwrap(); let mut out = Vec::new(); for count in (1..=max_count).rev() { // 遍历所有的小写字母 for byte in b'a'..=b'z' { let index = byte as usize; if chars[index] == count { out.push(format!("{}:{count}", char::from_u32(byte as u32).unwrap())); } } // 遍历所有的大写字母 for byte in b'A'..=b'Z' { let index = byte as usize; if chars[index] == count { out.push(format!("{}:{count}", char::from_u32(byte as u32).unwrap())); } } } // 打印结果 let s = out.join(";"); println!("{s}"); }
C++
#include <array>
#include <iostream>
#include <vector>
int main() {
// 创建计数数组
// 读取输入, 并统计所有字母出现的次数
// 统计出现的最多次数
// 然后逆向遍历所有的次数, 并打印结果
const size_t kNumChars = 256;
std::array<int, kNumChars> chars;
for (int i = 0; i < kNumChars; ++i) {
chars[i] = 0;
}
std::string line;
std::getline(std::cin, line);
for (char chr : line) {
int index = static_cast<int>(chr);
chars[index] += 1;
}
const int max_count = *std::max(chars.cbegin(), chars.cend());
std::vector<std::string> out;
const size_t kBufLen = 64;
char buf[kBufLen];
for (int count = max_count; count > 0; --count) {
// 遍历所有的小写字母
for (int char_index = static_cast<int>('a'); char_index <= static_cast<int>('z'); ++char_index) {
if (chars[char_index] == count) {
const int s_len = std::snprintf(buf, kBufLen, "%c:%d", static_cast<char>(char_index), count);
out.emplace_back(buf, s_len);
}
}
// 再遍历所有的大写字母
for (int char_index = static_cast<int>('A'); char_index <= static_cast<int>('Z'); ++char_index) {
if (chars[char_index] == count) {
const int s_len = std::snprintf(buf, kBufLen, "%c:%d", static_cast<char>(char_index), count);
out.emplace_back(buf, s_len);
}
}
}
// 打印结果
for (int i = 0; i + 1 < out.size(); ++i) {
std::cout << out[i] << ";";
}
std::cout << out[out.size() - 1] << std::endl;
return 0;
}
VLAN资源池
题目描述
VLAN是一种对局域网设备进行逻辑划分的技术, 为了标识不同的VLAN, 引入VLAN ID(1-4094之间的整数)的概念.
定义一个VLAN ID的资源池(下称VLAN资源池), 资源池中连续的VLAN用开始VLAN-结束VLAN表示, 不连续的用单个整数表示, 所有的VLAN用英文逗号连接起来.
现在有一个VLAN资源池, 业务需要从资源池中申请一个VLAN, 需要你输出从VLAN资源池中移除申请的VLAN后的资源池.
输入描述
第一行为字符串格式的VLAN资源池, 第二行为业务要申请的VLAN, VLAN的取值范围为 [1,4094]
之间的整数.
输出描述
从输入VLAN资源池中移除申请的VLAN后字符串格式的VLAN资源池, 输出要求满足题目描述中的格式, 并且按照VLAN从小到大升序输出. 如果申请的VLAN不在原VLAN资源池内, 输出原VLAN资源池升序排序后的字符串即可.
示例1
输入:
1-5
2
输出:
1,3-5
说明:
原VLAN资源池中有VLAN 1/2/3/4/5, 从资源池中移除2后, 剩下VLAN 1/3/4/5, 按照题目描述格式并升序后的结果为 1,3-5
示例2
输入:
20-21,15,18,30,5-10
15
输出:
5-10,18,20-21,30
说明:
原VLAN资源池中有VLAN 5/6/7/8/9/10/15/18/20/21/30, 从资源池中移除15后, 资源池中剩下的VLAN为
5/6/7/8/9/10/18/20/21/30, 按照题目描述格式并升序后的结果为 5-10,18,20-21,30
.
示例3
输入:
5,1-3
10
输出:
1-3,5
题解
Python
def main():
# 读取输入
# 解析当前所有的 VLAN ID, 并存储到集合或者数组中
# 然后移除指定的 ID
# 最后新剩下的 ID 格式化输出
parts = input().split(",")
id_set = set()
for part in parts:
if "-" in part:
range_part = part.split("-")
start_id = int(range_part[0])
end_id = int(range_part[1])
for vlan_id in range(start_id, end_id + 1):
assert 1 <= vlan_id <= 4094
id_set.add(vlan_id)
else:
vlan_id = int(part)
assert 1 <= vlan_id <= 4094
id_set.add(vlan_id)
assert id_set
removed_id = int(input())
if removed_id in id_set:
id_set.remove(removed_id)
# 格式化输出
# 先转换成列表, 再排序
id_list = list(id_set)
id_list.sort()
start_id = -1
last_id = -1
out = []
for vlan_id in id_list:
if last_id + 1 == vlan_id:
# 连续 ID
last_id = vlan_id
else:
# 重置连续 ID
if last_id == -1:
pass
elif last_id == start_id:
# 单个值
out.append(str(last_id))
else:
# 范围
out.append(F"{start_id}-{last_id}")
start_id = vlan_id
last_id = vlan_id
# 处理最后一个元素
if last_id == start_id:
# 单个值
out.append(str(last_id))
else:
# 范围
out.append(F"{start_id}-{last_id}")
# 打印结果
print(",".join(out))
if __name__ == "__main__":
main()
Rust
use std::collections::HashSet; use std::io::{stdin, BufRead}; fn solution() { // 读取输入 // 解析当前所有的 VLAN ID, 并存储到集合或者数组中 // 然后移除指定的 ID // 最后新剩下的 ID 格式化输出 let mut line = String::new(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let mut id_set = HashSet::<u32>::new(); for part in line.trim().split(",") { if part.contains("-") { let mut range_part = part.split("-"); let start_id: u32 = range_part.next().unwrap().parse().unwrap(); let end_id: u32 = range_part.next().unwrap().parse().unwrap(); assert!(range_part.next().is_none()); for vlan_id in start_id..=end_id { assert!((1..=4094).contains(&vlan_id)); id_set.insert(vlan_id); } } else { let vlan_id: u32 = part.parse().unwrap(); assert!((1..=4094).contains(&vlan_id)); id_set.insert(vlan_id); } } assert!(!id_set.is_empty()); line.clear(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let removed_id: u32 = line.trim().parse().unwrap(); id_set.remove(&removed_id); // 格式化输出 // 先转换成列表, 再排序 let mut id_list: Vec<u32> = id_set.into_iter().collect(); id_list.sort_unstable(); let mut start_id = u32::MAX; let mut last_id = u32::MAX; let mut out = Vec::new(); for &vlan_id in &id_list { if vlan_id - 1 == last_id { // 连续 ID last_id = vlan_id; } else { // 重置连续 ID if last_id == u32::MAX { // 忽略 } else if last_id == start_id { // 单个值 out.push(last_id.to_string()); } else { // 范围 out.push(format!("{start_id}-{last_id}")); } start_id = vlan_id; last_id = vlan_id; } } // 处理最后一个元素 if last_id == start_id { // 单个值 out.push(last_id.to_string()); } else { // 范围 out.push(format!("{start_id}-{last_id}")); } // 打印结果 println!("{}", out.join(",")); } fn main() { solution(); }
C++
#include <iostream>
#include <string>
#include <sstream>
#include <unordered_set>
#include <vector>
int main() {
// 读取输入
// 解析当前所有的 VLAN ID, 并存储到集合或者数组中
// 然后移除指定的 ID
// 最后新剩下的 ID 格式化输出
std::string line;
std::getline(std::cin, line);
std::vector<int> id_list;
std::stringstream ss(line);
std::getline(std::cin, line);
const int removed_id = std::stoi(line);
while (std::getline(ss, line, ',')) {
std::cout << "line: " << line << std::endl;
const int index = line.find('-');
if (index == std::string::npos) {
const int vlan_id = std::stoi(line);
id_list.push_back(vlan_id);
} else {
const std::string start = line.substr(0, index);
const int start_id = std::stoi(start);
const std::string end = line.substr(index + 1);
const int end_id = std::stoi(end);
for (int vlan_id = start_id; vlan_id <= end_id; ++vlan_id) {
id_list.push_back(vlan_id);
}
}
}
// 排序
std::sort(id_list.begin(), id_list.end());
// 移除指定的 ID
auto removed_id_iter = std::find(id_list.begin(), id_list.end(), removed_id);
if (removed_id_iter != id_list.end()) {
id_list.erase(removed_id_iter);
}
int start_id = -1;
int last_id = -1;
std::vector<std::string> out;
const size_t kBufLen = 64;
char buf[kBufLen + 1];
for (int vlan_id : id_list) {
if (last_id + 1 == vlan_id) {
// 连续 ID
last_id = vlan_id;
} else {
// 重置连续 ID
if (last_id == -1) {
// pass
} else if (last_id == start_id) {
// 单个值
out.push_back(std::to_string(start_id));
} else {
// 范围
const int s_len = snprintf(buf, kBufLen, "%d-%d", start_id, last_id);
out.emplace_back(buf, s_len);
}
start_id = vlan_id;
last_id = vlan_id;
}
}
// 处理最后一个元素
if (last_id == start_id) {
// 单个值
out.push_back(std::to_string(start_id));
} else{
// 范围
const int s_len = snprintf(buf, kBufLen, "%d-%d", start_id, last_id);
out.emplace_back(buf, s_len);
}
for (int index = 0; index + 1 < out.size(); ++index) {
std::cout << out[index] << ",";
}
std::cout << out[out.size() - 1] << std::endl;
return 0;
}
2024年E卷200分
- 空栈压数 - 栈
- 工号不够用了怎么办 - 数学
- 最长连续方波信号 - 栈
- 计算疫情扩散时间 - 图
- 字符串化繁为简 - 字符串
- 通过软盘拷贝文件 - 动态规划
- 跳马 - BFS
- 字母组合/过滤组合字符串 - 字符串
- 最大社交距离
- 模拟目录管理
- 找单词 - DFS
- 导师请吃火锅 - DFS
- 最大相连男生数/学生方阵
- 数字游戏
- 云短信平台优惠活动 - DP
- 寻找符合要求的最长子串 - 滑动窗口
- 跳格子3 - 动态规划
- 二叉树计算 - 树
- 猴子吃桃/爱吃蟠桃的孙悟空 - 二分查找
- 机器人活动区域 - DFS
- 简易压缩算法/一种字符串压缩表示的解压 - 字符串
- 智能驾驶 - 二分查找
- 字符串拼接 - 字符串
- 项目排期 - DFS
- 文本统计分析
- 电脑病毒感染 - 图
- 周末爬山 - DFS
- 计算网络信号/信号强度 - BFS
- 九宫格按键输入
- 服务器广播/需要广播的服务器数量 - 并查集
空栈压数
题目描述
向一个空栈压入正整数, 每当压入一个整数时, 执行以下规则 (设: 栈顶至栈底整数依次编号为 n1, n2, ..., nx, 其中n1 为最新压入的整数)
- 如果 n1 = n2, 则 n1/n2全部出栈, 压入新数据 m, m = 2 * n1
- 如果 n1 = n2 + ... + ny (y的范围为[3, x]), 则 n1, n2, ..., ny 全部出栈, 压入新数据 m, m = 2 * n1.
- 如果上述规则都不满足, 则不做操作.
例如: 依次向栈压入 6/1/2/3:
- 当压入 2 时, 栈顶至栈底依次为 [2,1,6].
- 当压入 3 时, 3 = 2 + 1, 3/2/1 全部出栈, 重新入栈整数6, 此时栈顶至栈底依次为 [6, 6]; 6 = 6, 两个 6 全部出栈, 压入 12.
- 最终栈中只剩个元素 12.
输入
使用单个空格隔开的正整数的字符串, 如 5 6 7 8
, 左边的数字先入栈.
- 正整数大小为 [1, 2^31−1]
- 正整数个数为 [1,1000]
输出
最终栈中存留的元素值, 元素值使用单个空格隔开, 如 8 7 6 5
, 从左至右依次为栈顶至栈底的数字.
示例1
输入:
10 20 50 80 1 1
输出:
2 160
示例2
输入:
5 10 20 50 85 1
输出:
1 170
题解
Python
def main():
# 读取输入, 并将它们转换成正整数
# 然后创建一个空的栈
# 遍历所有的正整数, 依次入栈
# 每次入栈时做以下检查:
# 1. 如果 n1=n2, 则它们全出栈, 然后自动入栈 2 * n1
# 2. 如果 n1=sum(n2, n3..), 则它们全出栈, 然后自动入栈 2 * n1
# 最后打印栈中剩下的整数
numbers = list(map(int, input().split()))
stack = []
for number in numbers:
# 检查规则1
if stack and stack[-1] == number:
stack[-1] += number
continue
top_sum = 0
will_append = True
# 从栈顶开始求和
for i in range(len(stack) - 1, -1, -1):
top_sum += stack[i]
if top_sum > number:
# 不满足
break
elif top_sum == number:
# 满足规则2
for j in range(len(stack) - 1, i - 1, -1):
stack.pop()
stack.append(number * 2)
will_append = False
break
if will_append:
# 如果上面的规则不满足, 就把该整数入栈
stack.append(number)
# 打印结果
s = " ".join(map(str, reversed(stack)))
print(s)
if __name__ == "__main__":
main()
Rust
use std::cmp::Ordering; use std::io::{stdin, BufRead}; fn solution() { // 读取输入, 并将它们转换成正整数 // 然后创建一个空的栈 // 遍历所有的正整数, 依次入栈 // 每次入栈时做以下检查: // 1. 如果 n1=n2, 则它们全出栈, 然后自动入栈 2 * n1 // 2. 如果 n1=sum(n2, n3..), 则它们全出栈, 然后自动入栈 2 * n1 // 最后打印栈中剩下的整数 let mut line = String::new(); let ret = stdin().lock().read_line(&mut line); assert!(ret.is_ok()); let numbers: Vec<i32> = line .split_ascii_whitespace() .map(|s| s.parse().unwrap()) .collect(); let mut stack: Vec<i32> = Vec::new(); for &number in &numbers { // 检查规则1 if stack.last() == Some(&number) { stack.pop(); stack.push(number * 2); continue; } let mut top_sum = 0; let mut will_append = true; // 从栈顶开始求和 for i in (0..stack.len()).rev() { top_sum += stack[i]; match top_sum.cmp(&number) { Ordering::Greater => { // 不满足 break; } Ordering::Equal => { // 满足规则2 stack.resize(i, 0); stack.push(number * 2); will_append = false; break; } _ => (), } } if will_append { // 如果上面的规则不满足, 就把该整数入栈 stack.push(number); } } // 打印结果 let out = stack .into_iter() .rev() .map(|x| x.to_string()) .collect::<Vec<_>>() .join(" "); println!("{out}"); } fn main() { solution(); }
工号不够用了怎么办
题目描述
3020年, 空间通信集团的员工人数突破20亿人, 即将遇到现有工号不够用的窘境.
现在, 请你负责调研新工号系统. 继承历史传统, 新的工号系统由小写英文字母 a-z 和数字 0-9 两部分构成.
新工号由一段英文字母开头, 之后跟随一段数字, 比如 aaahw0001
, a12345
, abcd1
, a00
.
注意新工号不能全为字母或者数字, 允许数字部分有前导0或者全为0.
但是过长的工号会增加同事们的记忆成本, 现在给出新工号至少需要分配的人数X和新工号中字母的长度Y, 求新工号中数字的最短长度Z.
输入描述
- 一行两个非负整数 X Y, 用数字用单个空格分隔
- 0< X <=2^50 – 1
- 0< Y <= 5
输出描述
输出新工号中数字的最短长度Z.
示例1
输入:
260 1
输出:
1
示例2
输入:
26 1
输出:
1
示例3
输入:
2600 1
输出:
2
题解
最长连续方波信号
题目描述
输入一串方波信号, 求取最长的完全连续交替方波信号, 并将其输出, 如果有相同长度的交替方波信号, 输出任一即可. 方波信号高位用1标识, 低位用0标识.
说明:
- 一个完整的信号一定以0开始然后以0结尾, 即010是一个完整信号, 但101, 1010, 0101不是
- 输入的一串方波信号是由一个或多个完整信号组成
- 两个相邻信号之间可能有0个或多个低位, 如0110010, 011000010
- 同一个信号中可以有连续的高位, 如01110101011110001010, 前14位是一个具有连续高位的信号
- 完全连续交替方波是指10交替, 如01010是完全连续交替方波, 0110不是
输入
输入信号字符串, 长度 >= 3 且 <= 1024:
例如: 0010101010110000101000010
注: 输入总是合法的, 不用考虑异常情况.
输出
输出最长的完全连续交替方波信号串.
例如: 01010
若不存在完全连续交替方波信号串, 输出 -1.
示例1
输入:
00101010101100001010010
输出:
01010
题解
Python
def main():
# 读取输入
# 然后使用双指针法遍历所有的输入信号
# 方波条件:
# 1. 以0开头, 以0结尾
# 2. 0和1交替出现
ZERO = "0"
ONE = "1"
signals = input()
assert 3 <= len(signals) <= 1024
assert signals[0] == ZERO and signals[-1] == ZERO
last_continous_signal = "-1"
stack = []
# 遍历输入信号
for char in signals:
# 如果栈为空
if len(stack) == 0:
# 那第一个信号需要是 "0"
# 如果不是"0", 就什么都不做
if char == ZERO:
print("stack bottom:", char)
stack.append(char)
continue
# 如果栈顶的信号与当前信号相同, 则说明出现了冲突
if stack[-1] == char:
# 检查当前栈中是不是有效的连续交替信号
# 如果是, 就把它更新到结果中
if stack[-1] == ZERO:
# 至少是 "010"
if len(stack) >= 3 and len(stack) > len(last_continous_signal):
last_continous_signal = "".join(stack)
elif len(stack) >= 4:
# 至少是 "0101"
stack.pop()
assert stack[-1] == ZERO
if len(stack) > len(last_continous_signal):
last_continous_signal = "".join(stack)
# 最后将栈顶清空, 如果当前元素是 "0", 就把它入栈; 否则什么也不做
stack.clear()
if char == ZERO:
stack.append(char)
continue
else:
# 没有出现相同信号, 将新的信号入栈即可
stack.append(char)
# 输出结果
print(last_continous_signal)
if __name__ == "__main__":
main()
计算疫情扩散时间
题目描述
在一个地图中, 地图由n*n个区域组成, 有部分区域被感染病菌. 感染区域每天都会把周围 (上下左右) 的4个区域感染.
请根据给定的地图计算, 多少天以后, 全部区域都会被感染. 如果初始地图上所有区域全部都被感染, 或者没有被感染区域, 返回-1.
输入描述
一行N*N个数字, 只包含0,1, 不会有其他数字, 表示一个地图, 数字间用,分割, 0表示未感染区域, 1表示已经感染区域.
每N个数字表示地图中一行, 输入数据共表示N行N列的区域地图.
例如输入 1,0,1,0,0,0,1,0,1
, 表示地图:
1,0,1
0,0,0
1,0,1
输出描述
一个整数, 表示经过多少天以后, 全部区域都被感染 1<=N<200.
示例1
输入:
1,0,1,0,0,0,1,0,1
输出:
2
说明: 1天以后, 地图中仅剩余中心点未被感染; 2天以后全部被感染.
示例2
输入:
1,1,1,1,1,1,1,1,1
输出:
-1
说明: 全部都感染.
题解
Python
import math
def main():
# 先读取输入
# 得到一个 N*N 的二维数组
array = list(map(int, input().split(",")))
AFFECTED = 1
UNAFFECTED = 0
# 然后检查感染区域, 如果全是0或者全是1, 就返回-1
# 否则就模拟每天的感染情况
all_affected = all(x == AFFECTED for x in array)
all_unaffected= all(x == UNAFFECTED for x in array)
if all_affected or all_unaffected:
print(-1)
return
# 计算二维数组的边界
rows = int(math.sqrt(len(array)))
columns = rows
assert rows * rows == len(array)
assert 1 <= rows < 200
# 将一维数组转换成二维数组, 方便进行定位
matrix = [array[row * columns: (row + 1) * columns] for row in range(rows)]
# 可能的感染方向
directions = ((1, 0), (-1, 0), (0, 1), (0, -1))
# 创建新一天的感染快照, 注意这里使用深拷贝, 解除两个数组间的关联
snapshot = matrix[:]
# 持续感染的天数
days = 0
# 计算还未被感染的区域数量
unaffected_remains = len(array) - len(list(x for x in array if x == UNAFFECTED))
# 持续感染, 直到扩散到所有区域
while unaffected_remains > 0:
for row in range(rows):
for col in range(columns):
if matrix[row][col] == UNAFFECTED:
# 当前区域尚未被感染, 等待被感染
pass
else:
# 去感染相邻四周
for dx, dy in directions:
row2 = row + dx
col2 = col + dy
# 检查新区域的有效性, 它是否已被感染
if 0 <= row2 < rows and 0 <= col2 < columns and snapshot[row2][col2] == UNAFFECTED:
snapshot[row2][col2] = AFFECTED
unaffected_remains -= 1
# 更新当天的感染情况
days += 1
# 注意这里是深拷贝
matrix = snapshot[:]
print(days)
if __name__ == "__main__":
main()
字符串化繁为简
题目描述
给定一个输入字符串, 字符串只可能由英文字母 (a-z
, A-Z
) 和左右小括号 (
, )
组成.
当字符里存在小括号时, 小括号是成对的, 可以有一个或多个小括号对, 小括号对不会嵌套, 小括号对内可以包含1个或多个英文字母, 也可以不包含英文字母.
当小括号对内包含多个英文字母时, 这些字母之间是相互等效的关系, 而且等效关系可以在不同的小括号对之间传递, 即当存在 ‘a’ 和 ‘b’, 等效和存在 ‘b’ 和 ‘c’ 等效时, ‘a’ 和 ‘c’ 也等效, 另外, 同一个英文字母的大写字母和小写字母也相互等效, 即使它们分布在不同的括号对里.
需要对这个输入字符串做简化, 输出一个新的字符串, 输出字符串里只需保留输入字符串里的没有被小括号对包含的字符, 按照输入字符串里的字符顺序, 并将每个字符替换为在小括号对里包含的且字典序最小的等效字符.
如果简化后的字符串为空, 请输出为"0".
示例:
输入字符串为 never(dont)give(run)up(f)()
, 初始等效字符集合为 , o, n, t
, r, u, n
,
由于等效关系可以传递, 因此最终等效字符集合为 d, o, n, t, r, u
,
将输入字符串里的剩余部分按字典序最小的等效字符替换后得到 devedgivedp
.
输入描述
input_string
输入为1行, 代表输入字符串.
备注: 输入字符串的长度在1~100000之间.
输出描述
output_string
输出为1行, 代表输出字符串.
示例1
输入:
()abd
输出:
abd
说明: 输入字符串里没有被小括号包含的子字符串为abd
, 其中每个字符没有等效字符, 输出为abd
示例2
输入:
(abd)demand(fb)()for
输出:
aemanaaor
示例3
输入:
()happy(xyz)new(wxy)year(t)
输出:
happwnewwear
说明: 等效字符集为 x, y, z, w
, 输入字符串里没有被小括号包含的子字符串集合为 happynewyear
,
将其中字符替换为字典序最小的等效字符后输出为 happwnewwear
.
示例4
输入:
never(dont)give(run)up(f)()
输出:
devedgivedp
说明:
等效字符集为 a, A, b
, 输入字符里没有被小括号包含的子字符串集合为 abcdefgAC
, 将其中字符替换为字典序最小的等效字符后输出为
AAcdefgAC
.
题解
Python
# 简单的字符串替换
def main():
# 读取输入
s = input()
assert 1 < len(s) < 100000
# 然后解析所有的括号, 并读取里面的所有字符, 并对它们进行排序, 做成一个替换表
# 然后将它们从字符串s中移除
# 最后使用替换表将字符串s中的字符替换成最小序的字符
raw_chars = []
tab_chars = []
found_bracket = False
for char in s:
if char == "(":
found_bracket = True
elif char == ")":
found_bracket = False
elif found_bracket:
tab_chars.append(char)
else:
raw_chars.append(char)
raw_str = "".join(raw_chars)
# 对替换表进行排序, 可以找出最小序的字母
tab_chars.sort()
if len(tab_chars) > 1:
first_char = tab_chars[0]
tab_dict = dict((char, first_char) for char in tab_chars[1:])
if len(tab_dict) > 1:
raw_str = raw_str.translate(str.maketrans(tab_dict))
print(raw_str)
if __name__ == "__main__":
main()
通过软盘拷贝文件
题目描述
有一名科学家想要从一台古董电脑中拷贝文件到自己的电脑中加以研究.
但此电脑除了有一个3.5寸软盘驱动器以外, 没有任何手段可以将文件持贝出来, 而且只有一张软盘可以使用.
因此这一张软盘是唯一可以用来拷贝文件的载体.
科学家想要尽可能多地将计算机中的信息拷贝到软盘中, 做到软盘中文件内容总大小最大.
已知该软盘容量为1474560字节. 文件占用的软盘空间都是按块分配的, 每个块大小为512个字节. 一个块只能被一个文件使用. 拷贝到软盘中的文件必须是完整的, 且不能采取任何压缩技术.
输入描述
- 第1行为一个整数N, 表示计算机中的文件数量. 1 ≤ N ≤ 1000.
- 接下来的第2行到第N+1行 (共N行), 每行为一个整数, 表示每个文件的大小Si, 单位为字节.
- 0 ≤ i < N,0 ≤ Si
备注: 为了充分利用软盘空间, 将每个文件在软盘上占用的块记录到本子上. 即真正占用软盘空间的只有文件内容本身.
输出描述
科学家最多能拷贝的文件总大小.
示例1
输入:
3
737270
737272
737288
输出:
1474542
说明:
- 3个文件中, 每个文件实际占用的大小分别为737280字节, 737280字节, 737792字节
- 只能选取前两个文件, 总大小为1474542字节. 虽然后两个文件总大小更大且未超过1474560字节, 但因为实际占用的大小超过了1474560字节, 所以不能选后两个文件
示例2
输入:
6
400000
200000
200000
200000
400000
400000
输出:
1400000
说明:
- 从6个文件中选择3个大小为400000的文件和1个大小为200000的文件, 得到最大总大小为1400000
- 也可以选择2个大小为400000的文件和3个大小为200000的文件, 得到的总大小也是1400000
题解
跳马
题目描述
输入 m 和 n 两个数, m 和 n 表示一个 m*n 的棋盘. 输入棋盘内的数据. 棋盘中存在数字和.
两种字符, 如果是数字表示该位置是一匹马, 如果是.
表示该位置为空的, 棋盘内的数字表示为该马能走的最大步数.
例如棋盘内某个位置一个数字为 k, 表示该马只能移动 1~k 步的距离.
棋盘内的马移动类似于中国象棋中的马移动, 先在水平或者垂直方向上移动一格, 然后再将其移动到对角线位置.
棋盘内的马可以移动到同一个位置, 同一个位置可以有多匹马.
请问能否将棋盘上所有的马移动到同一个位置, 若可以请输入移动的最小步数. 若不可以输出 0.
输入描述
输入 m 和 n 两个数, m 和 n 表示一个 m*n 的棋盘. 输入棋盘内的数据. 棋盘中存在数字和.
两种字符, 如果是数字表示该位置是一匹马, 如果是 .
表示该位置为空的, 棋盘内的数字表示为该马能走的最大步数.
例如棋盘内某个位置一个数字为 k, 表示该马只能移动 1~k 步的距离.
棋盘内的马移动类似于中国象棋中的马移动, 先在水平或者垂直方向上移动一格, 然后再将其移动到对角线位置.
棋盘内的马可以移动到同一个位置, 同一个位置可以有多匹马.
请问能否将棋盘上所有的马移动到同一个位置, 若可以请输入移动的最小步数, 若不可以输出 0.
输出描述
能否将棋盘上所有的马移动到同一个位置, 若可以请输入移动的最小步数, 若不可以输出 0.
示例1
输入:
3 2
. .
2 .
. .
输出:
0
示例2
输入:
3 5
4 7 . 4 8
4 7 4 4 .
7 . . . .
输出:
17
题解
Python
from collections import deque
def main():
# 读取行列值
rows, columns = list(map(int, input().split()))
horse_matrix = []
for row in range(rows):
parts = input().split()
for column in range(columns):
if parts[column] != ".":
# 马可以移动的最大步数
steps = int(parts[column])
horse_matrix.append((row, column, steps))
# 可以移动的方向
directions = ((-1, -2), (-1, 2), (1, -2), (1, 2), (-2, -1), (-2, 1), (2, -1), (2, 1))
INITIAL_STEPS = 10 ** 9
def dfs(row, column, x, y, max_move_steps, dist, visited):
if row == x and column == y:
return dist
# 访问所有的方向
for dx, dy in directions:
x2 = x + dx
y2 = y + dy
# 检查新的坐标位置是否有效, 是否访问过
if 0 <= x2 < rows and 0 <= y2 < columns and dist < max_move_steps and (x2, y2) not in visited:
visited.add((x2, y2))
steps = dfs(row, column, x2, y2, max_move_steps, dist + 1, visited)
if steps > -1:
return steps
return -1
min_steps = INITIAL_STEPS
# 遍历每个位置
for row in range(rows):
for column in range(columns):
# 所有马移动的总步数
total_steps = 0
possible_move = True
# 遍历每只马
for x, y, move_steps in horse_matrix:
visited = set()
steps = dfs(row, column, x, y, move_steps, 0, visited)
print("steps:", steps)
if steps > -1:
total_steps += steps
break
else:
possible_move = False
if possible_move:
print("total steps:", total_steps)
min_steps = min(min_steps, total_steps)
print("min_steps:", min_steps)
if __name__ == "__main__":
main()
字母组合/过滤组合字符串
题目描述
每个数字关联多个字母, 关联关系如下:
- 0 关联 "a","b","c"
- 1 关联 "d","e","f"
- 2 关联 "g","h","i"
- 3 关联 "j","k","l"
- 4 关联 "m","n","o"
- 5 关联 "p","q","r"
- 6 关联 "s","t"
- 7 关联 "u","v"
- 8 关联 "w","x"
- 9 关联 "y","z"
输入一串数字后, 通过数字和字母的对应关系可以得到多个字母字符串, 要求按照数字的顺序组合字母字符串.
屏蔽字符串: 屏蔽字符串中的所有字母不能同时在输出的字符串出现, 如屏蔽字符串是abc, 则要求字符串中不能同时出现a,b,c, 但是允许同时出现a,b或a,c或b,c等.
给定一个数字字符串和一个屏蔽字符串, 输出所有可能的字符组合.
例如输入数字字符串78和屏蔽字符串ux, 输出结果为uw, vw, vx. 数字字符串78, 可以得到如下字符串uw, ux, vw, vx. 由于ux是屏蔽字符串, 因此排除ux, 最终的输出是uw, vw, vx;
输入描述
第一行输入为一串数字字符串, 数字字符串中的数字不允许重复, 数字字符串的长度大于0, 小于等于5.
第二行输入是屏蔽字符串, 屏蔽字符串的长度一定小于数字字符串的长度, 屏蔽字符串中字符不会重复.
输出描述
输出可能的字符串组合.
注: 字符串之间使用逗号隔开, 最后一个字符串后携带逗号.
示例1
输入:
78
ux
输出:
uw,vw,vx,
示例2
输入:
78
x
输出:
uw,vw,
题解
Python
from itertools import permutations
def main():
# 读取输入
# 建立数字到字母的映射关系
# 就可以得到输入数字所有可能的字符串
# 然后去掉被屏蔽的字符串, 就得到了结果
num_str = input()
assert 0 < len(num_str) < 5
filter_str = input()
num_digits = list(map(int, num_str))
digit_mapping = ["abc", "def", "ghi", "jkl", "mno", "pqr", "st", "uv", "wx", "yz"]
assert len(digit_mapping) == 10
# 生成数字到字母的映射
letters = [digit_mapping[digit] for digit in num_digits]
def dfs(letters, letter_index, path, result, visited):
# 如果所有的字母都访问过了, 那就返回
if letter_index >= len(letters):
# 将当前路径上的字母加入到结果中
substr = "".join(path)
# 过滤字符串
if filter_str not in substr:
result.append(substr)
return
# 遍历当前索引位置的所有字母
for char in letters[letter_index]:
# 如果
if char not in visited:
path.append(char)
visited.add(char)
# 访问下一个字母组合
dfs(letters, letter_index + 1, path, result, visited)
# 将字母从临时路径中移除
path.pop()
# 将字母从访问过的记录中移除
visited.remove(char)
# 存放最后的结果
result = []
# 临时存放经过的路径
path = []
# 标记已访问过的字母
visited = set()
dfs(letters, 0, path, result, visited)
# 打印结果
print(",".join(result), ",", sep="")
if __name__ == "__main__":
main()
最大社交距离
题目描述
疫情期间需要大家保证一定的社交距离, 公司组织开交流会议. 座位一排共 N 个座位, 编号分别为 [0, N-1],
要求员工一个接着一个进入会议室, 并且可以在任何时候离开会议室.
满足:
- 每当一个员工进入时, 需要坐到最大社交距离, 最大化自己和其他人的距离的座位
- 如果有多个这样的座位, 则坐到索引最小的那个座位
输入描述
- 会议室座位总数 seatNum
- 1 ≤ seatNum ≤ 500
- 员工的进出顺序 seatOrLeave 数组
- 元素值为 1, 表示进场
- 元素值为负数, 表示出场, 特殊位置 0 的员工不会离开
- 例如 -4 表示坐在位置 4 的员工离开, 保证有员工坐在该座位上
输出描述
最后进来员工, 他会坐在第几个位置, 如果位置已满, 则输出-1.
示例1
输入:
10
[1, 1, 1, 1, -4, 1]
输出:
5
题解
Python
def main():
# 读取输入
# 座位的数量
num_seat = int(input())
assert 1 <= num_seat <= 500
# 进出的序列
line = input()[1:-1]
# 1 表示进入会议室, 负数表示从该位置离开会议室
enter_leave_list = list(map(int, line.split(",")))
assert enter_leave_list
# 下一个进入的员工可以占用的位置
new_position = -1
# 当前在会议室的所有员工占用的位置
# 这个序列在有员工进出时都要更新
seat = []
# 遍历所有进出序列
for op in enter_leave_list:
# 有员工离开
if op < 0:
# 得到离开员工的位置
position = -op
# 并将该位置移除
seat.remove(position)
continue
# 有员工进入会议室, 给他安排位置
assert op == 1
if not seat:
# 当会议室为空时, 要坐在位置0, 而且之后这个位置上的员工不再变动
new_position = 0
elif len(seat) == num_seat:
# 当会议室满了后, 新进入的员工没有位置
new_position = -1
else:
# 找出最大空闲位置
max_dist = 0
new_position = 0
# 遍历已有位置序列
for index, position in enumerate(seat):
# 当前位置的距离
distance = 0
if index + 1 == len(seat):
# 最后一个位置到当前位置的距离
distance = num_seat - 1 - position
else:
# 相邻两个位置的中间距离
distance = (seat[index + 1] - position) // 2
# 更新最大距离
if distance > max_dist:
max_dist = distance
# 更新新进入员工的位置
if index + 1 == len(seat):
# 最后一个位置
new_position = num_seat - 1
else:
# 相邻两个位置的中间位置
new_position = position + distance
if new_position != -1:
# 将新进入的员工排好位
seat.append(new_position)
seat.sort()
# 打印最终的结果
print(new_position)
if __name__ == "__main__":
main()
模拟目录管理
题目描述
实现一个模拟目录管理功能的软件, 输入一个命令序列, 输出最后一条命令运行结果.
支持命令:
- 创建目录命令:
mkdir 目录名称
, 如mkdir abc
为在当前目录创建abc目录, 如果已存在同名目录则不执行任何操作. 此命令无输出. - 进入目录命令:
cd 目录名称
, 如cd abc
为进入abc目录, 特别地,cd ..
为返回上级目录, 如果目录不存在则不执行任何操作. 此命令无输出. - 查看当前所在路径命令:
pwd
, 输出当前路径字符串.
约束:
- 目录名称仅支持小写字母
- mkdir 和 cd 命令的参数仅支持单个目录, 如:
mkdir abc
和cd abc
; 不支持嵌套路径和绝对路径, 如mkdir abc/efg
,cd abc/efg
,mkdir /abc/efg
,cd /abc/efg
是不支持的. - 目录符号为
/
, 根目录/
作为初始目录. - 任何不符合上述定义的无效命令不做任何处理并且无输出
输入描述
输入 N 行字符串, 每一行字符串是一条命令.
输出描述
输出最后一条命令运行结果字符串.
备注
命令行数限制100行以内, 目录名称限制10个字符以内.
用例1
输入:
mkdir abc
cd abc
pwd
输入:
/abc/
说明: 在根目录创建一个abc的目录并进入abc目录中查看当前目录路径, 输出当前路径 /abc/
题解
Python
import string
import sys
class FileNode:
def __init__(self, path, parent=None):
# 当前目录的绝对路径
self.path = path
# 指向父目录节点
self.parent = parent
# 子目录节点, 默认为空
self.children = {}
# 创建特属的子节点, 指向父目录
if self.parent:
self.children[".."] = self.parent
def validate_folder_name(self, folder_name) -> bool:
# 检查目录名是否包含无效字符
for char in folder_name:
if char not in string.ascii_lowercase:
return False
return True
def mkdir(self, folder_name):
if not self.validate_folder_name(folder_name):
return False
# 检查相同的目录名是否已经存在
if folder_name in self.children:
return False
# 创建新的目录节点, 并存储到子目录中
path = self.path + folder_name + "/"
new_folder = FileNode(path, self)
self.children[folder_name] = new_folder
return True
def cd(self, folder_name):
# 进入到父目录
if folder_name == "..":
return True, self.parent
# 校验目录名
if not self.validate_folder_name(folder_name):
return False, self
# 未找到子目录
if folder_name not in self.children:
return False, self
return True, self.children[folder_name]
def main():
# 首先创建根目录
root = FileNode("/")
# 创建根目录的引用, 当前工作目录
cwd = root
# 然后依次读取输入, 如果输入无效, 则直接忽略, 并继续读取下一条输入
for line in sys.stdin:
line = line.strip()
if not line:
continue
# 解析命令
parts = line.split()
cmd = parts[0]
if cmd == "pwd":
# 打印当前的目录
if len(parts) == 1:
print(cwd.path)
elif cmd == "cd":
if len(parts) == 2:
# 切换工作目录
folder_name = parts[1]
ok, new_cwd = cwd.cd(folder_name)
if not ok:
print("[cd] Invalid command:", line)
else:
cwd = new_cwd
elif cmd == "mkdir":
# 创建子目录
if len(parts) == 2:
folder_name = parts[1]
ok = cwd.mkdir(folder_name)
if not ok:
print("[mkdir] Invalid command:", line)
if __name__ == "__main__":
main()
Rust
use std::collections::{HashMap, HashSet}; use std::io::{stdin, BufRead}; /// 目录节点 pub struct FolderEntry { /// 当前目录的绝对路径 path: String, /// 指向父节点 parent: String, /// 子节点 children: HashSet<String>, } pub type FolderMap = HashMap<String, FolderEntry>; impl FolderEntry { #[must_use] #[inline] pub fn new(path: String, parent: String) -> Self { Self { path, parent, children: HashSet::new(), } } fn validate_folder_name(folder_name: &str) -> bool { for chr in folder_name.chars() { if !chr.is_ascii_lowercase() { return false; } } true } pub fn mkdir(&mut self, folder_name: String) -> Result<Self, String> { if !Self::validate_folder_name(&folder_name) { return Err(folder_name); } if self.children.contains(&folder_name) { return Err(folder_name); } let path = Self::to_path(&self.path, &folder_name); self.children.insert(folder_name); Ok(Self::new(path, self.path.clone())) } fn to_path(path: &str, folder_name: &str) -> String { format!("{path}{folder_name}/") } pub fn cd(&self, folder_name: &str) -> Option<String> { if folder_name == ".." { return Some(self.parent.clone()); } if !Self::validate_folder_name(folder_name) { return None; } if self.children.contains(folder_name) { let path = Self::to_path(&self.path, folder_name); Some(path) } else { None } } } fn solution() { let root = FolderEntry::new("/".to_owned(), "/".to_owned()); let mut map = HashMap::new(); let mut cwd = root.path.clone(); map.insert(root.path.clone(), root); for line in stdin().lock().lines() { let line = line.unwrap(); let line = line.trim_ascii(); if line.is_empty() { continue; } let mut parts = line.split_ascii_whitespace(); match parts.next() { Some("pwd") => { if parts.next().is_none() { println!("pwd: {}", cwd); } } Some("cd") => { // 切换工作目录 if let Some(folder_name) = parts.next() { if parts.next().is_some() { continue; } if let Some(cwd_entry) = map.get_mut(&cwd) { if let Some(new_folder_name) = cwd_entry.cd(folder_name) { cwd = new_folder_name; } } } } Some("mkdir") => { // 创建子目录 if let Some(folder_name) = parts.next() { if parts.next().is_some() { continue; } if let Some(cwd_entry) = map.get_mut(&cwd) { if let Ok(new_folder) = cwd_entry.mkdir(folder_name.to_owned()) { map.insert(new_folder.path.clone(), new_folder); } } } } Some(_) | None => {} } } } fn main() { solution() }
找单词
题目描述
给一个字符串和一个二维字符数组, 如果该字符串存在于该数组中, 则按字符串的字符顺序输出字符串每个字符所在单元格的位置下标字符串, 如果找不到返回字符串 N.
- 需要按照字符串的字符组成顺序搜索, 且搜索到的位置必须是相邻单元格, 其中
相邻单元格
是指那些水平相邻或垂直相邻的单元格 - 同一个单元格内的字母不允许被重复使用
- 假定在数组中最多只存在一个可能的匹配
输入描述
- 第1行为一个数字N指示二维数组在后续输入所占的行数
- 第2行到第N+1行输入为一个二维大写字符数组, 每行字符用半角, 分割
- 第N+2行为待查找的字符串, 由大写字符组成
- 二维数组的大小为N*N, 0<N<=100
- 单词长度K, 0<K<1000
输出描述
出一个位置下标字符串, 拼接格式为:
第1个字符行下标 + ,
+ 第1个字符列下标 + ,
+ 第2个字符行下标 + ,
+ 第2个字符列下标… + 第N个字符列下标
示例1
输入:
4
A,C,C,F
C,D,E,D
B,E,S,S
F,E,C,A
ACCESS
输出:
0,0,0,1,0,2,1,2,2,2,2,3
题解
Python
def main():
# 读取字母的行数
rows = int(input())
# 字母表, N * N 的二维数组
table = []
for _i in range(rows):
# 读取当前行的所有字母
table.append(list(input().split(",")))
assert len(table[-1]) == rows
# 读取输入的单词
word = input()
# 用于标记已经访问过的字符, N * N
visited = [[False] * rows for _row in range(rows)]
# 定义查找的四个方向
directions = ((1, 0), (-1, 0), (0, 1), (0, -1))
# 使用DFS搜索所有可能的位置
def dfs(row: int, column: int, index_in_word: int, path):
# 结束查找
# 1. 检查坐标的边界, 如果不在 table 内
# 2. 或者已经访问过了
# 3. 或者单词中的字母与在表格中的不一致
if row < 0 or row >= rows or column < 0 or column >= rows or \
visited[row][column] or \
word[index_in_word] != table[row][column]:
return False
# 将当前路径添加到 path 中
path.append((row, column))
# 并标记该节点已经访问过
visited[row][column] = True
# 如果单词中的所有字母都被找到了, 就返回
if index_in_word + 1 == len(word):
return True
# 遍历所有可能的方向, 进行深入查找
for direction in directions:
row2 = row + direction[0]
column2 = column + direction[1]
# 去找单词中的下一个字母
found = dfs(row2, column2, index_in_word + 1, path)
# 如果在该方向找到了字符串, 就直接返回
if found:
return True
# 没有找到, 当前前位置从经过的路径中移除
path.pop()
# 并将该坐标从被访问记录中移除
visited[row][column] = False
return False
def find_string():
# 用于存储访问路径
path= []
# 遍历所有的单元格
for row in range(rows):
for column in range(rows):
# 如果当前单元格的字符等于单词的第一个字母
if table[row][column] == word[0]:
# 使用DFS查找字符串
found = dfs(row, column, 0, path)
if found:
# 找到了, 就返回结果
positions = []
for row, column in path:
positions.append(str(row))
positions.append(str(column))
result = ",".join(positions)
return result
# 没有找到合适的
return "N"
result = find_string()
# 打印最后的结果
print(result)
if __name__ == "__main__":
main()
C++
#include <cassert>
#include <array>
#include <iostream>
#include <vector>
bool dfs(const std::vector<std::vector<char>>& table,
std::vector<std::vector<bool>>& visited,
std::vector<std::pair<int, int>>& path,
const std::string& word, int word_index,
int row, int column) {
const int rows = table.size();
const int columns = rows;
// 结束查找:
// 1. 检查坐标的边界, 如果不在 table 内
// 2. 或者已经访问过了
// 3. 或者单词中的字母与在表格中的不一致
if (row < 0 || row >= rows || column < 0 || column >= columns ||
visited[row][column] || word[word_index] != table[row][column]) {
return false;
}
// 将当前路径添加到 path 中
path.emplace_back(row, column);
// 并标记该节点已经访问过
visited[row][column] = true;
// 如果单词中的所有字母都被找到了, 就返回
if (word_index + 1 == word.size()) {
return true;
}
// 定义查找的四个方向
const std::array<std::array<int, 2>, 4> directions = {
std::array<int, 2>{1, 0}, {-1, 0}, {0, 1}, {0, -1}
};
// 遍历所有可能的方向, 进行深入查找
for (const std::array<int, 2> dir : directions) {
const int row1 = row + dir[0];
const int column1 = column + dir[1];
// 去找单词中的下一个字母
const bool found = dfs(table, visited, path, word, word_index + 1, row1, column1);
// 如果在该方向找到了字符串, 就直接返回
if (found) {
return true;
}
}
// 没有找到, 当前前位置从经过的路径中移除
path.pop_back();
// 并将该坐标从被访问记录中移除
visited[row][column] = false;
return false;
}
void solution() {
// 读取输入
int rows = 0;
std::cin >> rows;
// 换行符
std::string line;
std::cin >> line;
// 读取所有字符表
std::vector<std::vector<char>> table;
for (int row = 0; row < rows; ++row) {
std::vector<char> row_chars(rows);
for (char& c : row_chars) {
std::cin >> c;
}
// 换行符
std::cin >> line;
table.emplace_back(row_chars);
}
// 读取单词
std::string word;
std::cin >> word;
// 用于标记已经访问过的字符, N * N
std::vector<std::vector<bool>> visited(rows, std::vector<bool>(rows));
// 用于存储访问路径
std::vector<std::pair<int, int>> path;
const int columns = rows;
// 遍历所有字符
for (int row = 0; row < rows; ++row) {
for (int column = 0; column < columns; ++column) {
// 如果当前单元格的字符等于单词的第一个字母
if (word[0] == table[row][column]) {
// 使用DFS查找字符串
const bool found = dfs(table, visited, path, word, 0, row, column);
if (found) {
// 找到了, 就返回结果
for (const auto& pair : path) {
std::cout << pair.first << "," << pair.second << std::endl;
}
return;
}
}
}
}
std::cout << "N" << std::endl;
}
int main() {
// FIXME(Shaohua): Result invalid
solution();
return 0;
}
导师请吃火锅
题目描述
入职后, 导师会请你吃饭, 你选择了火锅.
火锅里会在不同时间下很多菜.
不同食材要煮不同的时间, 才能变得刚好合适.
你希望吃到最多的刚好合适的菜, 但你的手速不够快, 用m代表手速, 每次下手捞菜后至少要过m秒才能再捞, 每次只能捞一个.
那么用最合理的策略, 最多能吃到多少刚好合适的菜?
输入描述
第一行两个整数n, m, 其中n代表往锅里下的菜的个数, m代表手速, 1 < n, m < 1000.
接下来有n行, 每行有两个数x, y代表第x秒下的菜过y秒才能变得刚好合适, 1 < x, y < 1000.
输出描述
输出一个整数代表用最合理的策略, 最多能吃到刚好合适的菜的数量.
示例1
输入:
2 1
1 2
2 1
输出:
1
题解
Python
import sys
def main():
# 先读取输入
parts = input().split()
assert len(parts) == 2
# 菜的个数
n = int(parts[0])
# 手速
m = int(parts[1])
# 放菜的策略, 每个菜可以食用的时间点
times = []
for line in sys.stdin.readlines():
start, delay = list(map(int, line.split()))
time = start + delay
times.append(time)
assert len(times) == n
# 每个时间点可以吃的菜的个数, 初始化为0
# 注意, 时间点是从1开始计数的, 我们这里要从0开始
food_nums = [0] * (max(times) + 1)
for time in times:
# 这个时间点有菜
food_nums[time] += 1
# 记录每种策略下可以吃到的菜的数量, 最后从中选择最大值就行
eat_food = []
# DFS 查找当前时间点可以吃的菜的数量
def dfs(time, current_food):
# 超过最大时间点, 后面没菜了, 终止递归
if time >= len(food_nums):
# 当前策略下吃到的所有的菜
eat_food.append(current_food)
return
elif food_nums[time] > 0:
# 当前时间点有菜, 有两个策略:
# 1. 直接吃, 然后等待m秒
dfs(time + m, current_food + 1)
# 2. 不吃, 去到下个时间点吃
dfs(time + 1, current_food)
else:
# 当前时间点没有菜, 去下个时间点
dfs(time + 1, current_food)
# 从第1个时间点开始进行递归搜索
dfs(1, 0)
# 打印可以吃到的最多的菜
print(max(eat_food))
if __name__ == "__main__":
main()
最大相连男生数/学生方阵
题目描述
学校组织活动, 将学生排成一个矩形方阵.
请在矩形方阵中找到最大的位置相连的男生数量.
这个相连位置在一个直线上, 方向可以是水平的, 垂直的, 成对角线的或者呈反对角线的.
注: 学生个数不会超过10000
输入描述
输入的第一行为矩阵的行数和列数, 接下来的n行为矩阵元素, 元素间用,
分隔.
输出描述
输出一个整数, 表示矩阵中最长的位置相连的男生个数.
示例1
输入:
3,4
F,M,M,F
F,M,M,F
F,F,F,M
输出:
3
说明:
题解
Python
def main():
# 读取输入
# 然后遍历二维数组中的所有节点, 找到 "M", 然后基于此, 向四个方向移动, 找到最长的连续队列
rows, columns = map(int, input().split(","))
assert 0 < rows and 0 < columns and rows * columns < 10000
table = [list(input().split(",")) for _row in range(rows)]
assert len(table[0]) == columns
MAN = "M"
WOMEN = "W"
# 当前节点可以连接最多男生的数量
count_list = []
# 查找的四个方向, 向右/向下/右下角/左下角
directions = ((1, 0), (0, 1), (1, 1), (-1, 1))
def get_max_male_student(x, y, count_list):
# 遍历所有方向
for dx, dy in directions:
x2 = x + dx
y2 = y + dy
# 男学生的数量
count = 1
# 按当前方向一直查找
# 1. 新的坐标在队列内
# 2. 新的位置是男生
while 0 <= x2 < rows and 0 <= y2 < columns and table[x2][y2] == MAN:
x2 += dx
y2 += dy
count += 1
count_list.append(count)
# 遍历所有节点
for row in range(rows):
for column in range(columns):
# 如果当前节点是位男生, 就递归地找最长连续序列
if table[row][column] == MAN:
get_max_male_student(row, column, count_list)
# 打印结果
print(max(count_list))
if __name__ == "__main__":
main()
数字游戏
题目描述
小明玩一个游戏.
系统发1+n张牌, 每张牌上有一个整数.
第一张给小明, 后n张按照发牌顺序排成连续的一行.
需要小明判断, 后n张牌中, 是否存在连续的若干张牌, 其和可以整除小明手中牌上的数字.
输入描述
输入数据有多组, 每组输入数据有两行, 输入到文件结尾结束.
第一行有两个整数n和m, 空格隔开. m代表发给小明牌上的数字.
第二行有n个数, 代表后续发的n张牌上的数字, 以空格隔开.
备注:
- 1 ≤ n ≤ 1000
- 1 ≤ 牌上的整数 ≤ 400000
- 输入的组数, 不多于1000
- 用例确保输入都正确, 不需要考虑非法情况
输出描述
对每组输入, 如果存在满足条件的连续若干张牌, 则输出1; 否则, 输出0.
云短信平台优惠活动
题目描述
某云短信厂商, 为庆祝国庆, 推出充值优惠活动. 现在给出客户预算, 和优惠售价序列, 求最多可获得的短信总条数.
输入描述
- 第一行客户预算M, 其中 0 ≤ M ≤ 10^6
- 第二行给出售价表, P1, P2, … Pn, 其中 1 ≤ n ≤ 100
- Pi为充值 i 元获得的短信条数. 1 ≤ Pi ≤ 1000 , 1 ≤ n ≤ 100
输出描述
最多获得的短信条数.
示例1
输入:
6
10 20 30 40 60
输出:
70
说明: 分两次充值最优, 1元, 5元各充一次, 总条数 10 + 60 = 70.
示例2
输入:
15
10 20 30 40 60 60 70 80 90 150
输出:
210
说明: 分两次充值最优, 10元, 5元各充一次, 总条数 150 + 60 = 210.
题解
寻找符合要求的最长子串
题目描述
给定一个字符串s, 找出这样一个子串:
- 该子串中任意一个字符最多出现2次
- 该子串不包含指定某个字符
请你找出满足该条件的最长子串的长度.
输入描述
- 第一行:要求不包含的指定字符, 为单个字符, 取值范围[0-9a-zA-Z]
- 第二行:字符串s, 每个字符范围[0-9a-zA-Z], 长度范围[1, 10000]
输出描述
- 第一行: 要求不包含的指定字符, 为单个字符, 取值范围[0-9a-zA-Z]
- 第二行: 字符串s, 每个字符范围[0-9a-zA-Z], 长度范围[1, 10000]
示例1
输入:
D
ABC123
输出:
6
示例2
输入:
D
ABACA123D
输出:
7
题解
Python
from collections import defaultdict
def main():
# 先读取输入值
ignored_char = input()
s = input()
assert len(ignored_char) == 1
assert 1 <= len(s) <= 10000
# 使用滑动窗口遍历字符串s中的所有字符
# 使用字典来统计各个字符出现的次数
# 当窗口右侧向右移动时, 将新的字符加到到计数字典中
# 当窗口左侧向右移动时, 将左侧旧的字符从计数字典中减1
# 判定条件有两个:
# 1. 不能出现 ignored_char
# 2. 窗口中同一个字符最大出现次数是2次
# 窗口中各字符的计数
window_chars = defaultdict(int)
left = 0
right = 0
substring_max_len = 0
# 窗口中同一个字符最大出现次数是2次
char_max_presents = 2
# 遍历字符串s
while right < len(s):
right_char = s[right]
# 如果窗口右侧出现了禁止的字符, 就说明这个窗口要被终止了
if right_char == ignored_char:
substring_max_len = max(substring_max_len, right - left)
# 将左右指针都移动到下一个字符
right += 1
left = right
# 重置计数字典
window_chars.clear()
continue
# 将当前字符加入到计数字典中
window_chars[right_char] += 1
# 如果当前字符出现次数超过 2 次, 就需要把窗口左侧向右移动
if window_chars[right_char] > char_max_presents:
substring_max_len = max(substring_max_len, right - left)
left_char = s[left]
while left_char != right_char:
window_chars[left_char] -= 1
left += 1
left_char = s[left]
# 最后, 将窗口右侧向右移
right += 1
# 最后一个子串
substring_max_len = max(substring_max_len, right - left)
print(substring_max_len)
if __name__ == "__main__":
main()
跳格子3
题目描述
小明和朋友们一起玩跳格子游戏, 每个格子上有特定的分数 score = [1, -1, -6, 7, -17, 7],
从起点score[0]开始, 每次最大的步长为k, 请你返回小明跳到终点 score[n-1] 时, 能得到的最大得分.
输入描述
- 第一行输入总的格子数量 n
- 第二行输入每个格子的分数 score[i]
- 第三行输入最大跳的步长 k
备注:
- 格子的总长度 n 和步长 k 的区间在 [1, 100000]
- 每个格子的分数 score[i] 在 [-10000, 10000] 区间中
输出描述
输出最大得分.
示例1
输入:
6
1 -1 -6 7 -17 7
2
输出:
14
题解
二叉树计算
题目描述
给出一个二叉树如下图所示:
请由该二叉树生成一个新的二叉树, 它满足其树中的每个节点将包含原始树中的左子树和右子树的和.
左子树表示该节点左侧叶子节点为根节点的一颗新树, 右子树表示该节点右侧叶子节点为根节点的一颗新树.
输入描述
2行整数, 第1行表示二叉树的中序遍历, 第2行表示二叉树的前序遍历, 以空格分割.
输出描述
1行整数, 表示求和树的中序遍历, 以空格分割.
示例1
输入:
-3 12 6 8 9 -10 -7
8 12 -3 6 -10 9 -7
输出:
0 3 0 7 0 2 0
题解
Python
class TreeNode:
def __init__(self, value: int, left=None, right=None):
self.value = value
self.left = left
self.right = right
def build_tree(preorder, inorder, length) -> TreeNode:
# 没有更多节点
if length == 0:
return None
# preorder[0] 是根节点, 现在确定根节点在 inorder 中的位置
k = 0
while preorder[0] != inorder[k]:
k += 1
# preorder 序列的内容, | root | left | right |
# inorder 序列的内容, | left | root | right |, 左子树有 k 个节点
# 构造二叉树
node = TreeNode(preorder[0])
node.left = build_tree(preorder[1: k + 1], inorder[: k], k)
# 注意要考虑根节点占用一个位置
node.right = build_tree(preorder[k + 1: ], inorder[k + 1: ], length - k - 1)
return node
# 以中序遍历的方式递归访问二叉树
def inorder_traversal(tree: TreeNode, out: list[int]):
if not tree:
return
inorder_traversal(tree.left)
out.append(tree.value)
inorder_traversal(tree.right)
def main():
# 读取序列
inorder = list(map(int, input().split()))
preorder = list(map(int, input().split()))
assert len(inorder) == len(preorder)
# 我们假定二叉树中的每个节点的值都是唯一的
# 以前序遍历序列和中序遍历序列来构造原来的二叉树
tree = build_tree(preorder, inorder, len(preorder))
out = []
inorder_traversal(tree, out)
if __name__ == "__main__":
main()
猴子吃桃/爱吃蟠桃的孙悟空
题目描述
孙悟空爱吃蟠桃, 有一天趁着蟠桃园守卫不在来偷吃. 已知蟠桃园有 N 棵桃树, 每颗树上都有桃子, 守卫将在 H 小时后回来.
孙悟空可以决定他吃蟠桃的速度K个/小时, 每个小时选一颗桃树, 并从树上吃掉 K 个, 如果树上的桃子少于 K 个, 则全部吃掉, 并且这一小时剩余的时间里不再吃桃.
孙悟空喜欢慢慢吃, 但又想在守卫回来前吃完桃子.
请返回孙悟空可以在 H 小时内吃掉所有桃子的最小速度 K, K为整数. 如果以任何速度都吃不完所有桃子, 则返回0.
输入描述
- 一行输入为 N 个数字, N 表示桃树的数量, 这 N 个数字表示每颗桃树上蟠桃的数量
- 第二行输入为一个数字, 表示守卫离开的时间 H
- 其中数字通过空格分割, N、H为正整数, 每颗树上都有蟠桃, 且 0 < N < 10000, 0 < H < 10000
输出描述
吃掉所有蟠桃的最小速度 K, 无解或输入异常时输出 0.
示例1
输入:
2 3 4 5
4
输出:
5
示例2
输入:
2 3 4 5
3
输出:
0
题解
Python
import math
def can_finish(peaches, leave_hours, eat_speed):
ans = 0
for peach in peaches:
# 每棵树上花的时间, 不够一个小时, 就算一个小时, 因为猴子要慢吃
ans += math.ceil(peach / eat_speed)
# 吃的总时间不大于离开的时间
return ans <= leave_hours
def main():
peaches = list(map(int, input().split()))
hours = int(input())
n = len(peaches)
if n == 0 or n >= 1000 or hours <= 0 or hours >= 10000:
print(0)
return
# 二分查找法找出吃的速度的最小值
left = 1
right = 10 ** 9
while left < right:
middle = left + (right - left) // 2
if can_finish(peaches, hours, middle):
right = middle
else:
left = middle + 1
# 如果最快的速度仍然吃不完, 那就无解
if left == right:
print(0)
else:
print(left)
if __name__ == "__main__":
main()
机器人活动区域
题目描述
现有一个机器人, 可放置于 M × N 的网格中任意位置, 每个网格包含一个非负整数编号, 当相邻网格的数字编号差值的绝对值小于等于 1 时, 机器人可以在网格间移动.
问题: 求机器人可活动的最大范围对应的网格点数目.
说明: 网格左上角坐标为 (0, 0), 右下角坐标为 (m−1, n−1), 机器人只能在相邻网格间上下左右移动.
输入描述
- 第 1 行输入为 M 和 N
- M 表示网格的行数
- N 表示网格的列数
- 之后 M 行表示网格数值, 每行 N 个数值 (数值大小用 k 表示), 数值间用单个空格分隔, 行首行尾无多余空格
- M, N, k 均为整数
- 1 ≤ M, N ≤ 150
- 0 ≤ k ≤ 50
输出描述
输出 1 行, 包含 1 个数字, 表示最大活动区域的网格点数目, 行首行尾无多余空格.
示例1
输入:
4 4
1 2 5 2
2 4 4 5
3 5 7 1
4 6 2 4
输出:
6
说明: 如下图, 图中红色区域, 相邻网格差值绝对值都小于等于 1, 且为最大区域, 对应网格点数目为 6.
示例2
输入:
2 3
1 3 5
4 1 3
输出:
1
说明: 任意两个相邻网格的差值绝对值都大于1, 机器人不能在网格间移动, 只能在单个网格内活动, 对应网格点数目为1.
题解
使用 广度优先搜索 BFS 求得最大的连接节点数.
Python
import sys
def main():
# 读取网格的行列值
parts = input().split()
rows = int(parts[0])
columns = int(parts[1])
assert 1 <= rows <= 150
assert 1 <= columns <= 150
# 读取网格中的数值
grid = []
for line in sys.stdin.readlines():
grid.append(list(map(int, line.split())))
assert len(grid[-1]) == columns
assert len(grid) == rows
print(grid)
# 标记已经访问过了的节点
visited = [[False] * columns for _row in range(rows)]
# 四个移动的方向
directions = ((1, 0), (-1, 0), (0, 1), (0, -1))
def dfs(grid, visited, x, y):
if visited[x][y]:
return 0
# 先标记当前节点已经访问过
visited[x][y] = True
move_range = 1
# 遍历四个可能的移动方向
for dx, dy in directions:
x1 = x + dx
y1 = y + dy
# 判断新的节点是否满足条件
# 1. 在矩形范围内移动
# 2. 新的节点未被访问过
# 3. 两个节点上的值相差小于等于1
if 0 <= x1 < rows and 0 <= y1 < columns and \
not visited[x1][y1] and abs(grid[x][y] - grid[x1][y1]) <= 1:
# 递归访问新的节点
move_range += dfs(grid, visited, x1, y1)
#print("move from:", x, y, ", to :", x1, y1)
# 返回最大能访问的节点数
return move_range
# 遍历所有的格子, 找到最大的移动范围
max_range = 0
for i in range(rows):
for j in range(columns):
# 使用DFS方法, 尝试向四个方向移动
move_range = dfs(grid, visited, i, j)
max_range = max(max_range, move_range)
print(max_range)
if __name__ == "__main__":
main()
C++
#include <cassert>
#include <array>
#include <iostream>
#include <string>
#include <vector>
int dfs_visit(const std::vector<std::vector<int>>& grid,
std::vector<std::vector<bool>>& visited,
int x, int y, int rows, int columns) {
if (visited[x][y]) {
return 0;
}
// 先标记当前节点状态
int move_range = 1;
visited[x][y] = true;
const std::array<std::array<int, 2>, 4> directions =
{std::array<int, 2>{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
// 开始准备向四个方向访问
for (const auto dir : directions) {
int x1 = x + dir[0];
int y1 = y + dir[1];
// 判断新的节点是否满足条件
// 1. 在矩形范围内移动
// 2. 新的节点未被访问过
// 3. 两个节点上的值相差小于等于1
if (0 <= x1 && x1 < rows && 0 <= y1 && y1 < columns && !visited[x1][y1] &&
std::abs(grid[x][y] - grid[x1][y1]) <= 1) {
// 递归访问新的节点
move_range += dfs_visit(grid, visited, x1, y1, rows, columns);
}
}
// 返回移动次数
return move_range;
}
void solution() {
// 读取行与列
int rows = 0;
int columns = 0;
std::cin >> rows >> columns;
assert(rows > 0 && columns > 0);
// 换行符
std::string line;
std::getline(std::cin, line);
// 读取二维数组
std::vector<std::vector<int>> grid;
for (int row = 0; row < rows; ++row) {
std::vector<int> nums(columns);
for (int& num : nums) {
std::cin >> num;
}
grid.emplace_back(nums);
// 换行符
std::getline(std::cin, line);
}
// 标记已经访问过了的节点
std::vector<std::vector<bool>> visited(rows, std::vector<bool>(columns, false));
int max_move_range = 0;
// 接下来遍历所有的节点, 找到最大的访问范围.
for (int row = 0; row < rows; ++row) {
for (int column = 0; column < columns; ++column) {
// 如果该节点已经访问过, 就忽略
if (!visited[row][column]) {
const int move_range = dfs_visit(grid, visited, row, column, rows, columns);
max_move_range = std::max(max_move_range, move_range);
}
}
}
// 打印结果
std::cout << max_move_range << std::endl;
}
int main() {
solution();
return 0;
}
简易压缩算法/一种字符串压缩表示的解压
题目描述
有一种简易压缩算法: 针对全部为小写英文字母组成的字符串, 将其中连续超过两个相同字母的部分压缩为连续个数加该字母 其他部分保持原样不变.
例如字符串 aaabbccccd
经过压缩变成字符串 3abb4cd
.
请您编写解压函数,根据输入的字符串,判断其是否为合法压缩过的字符串.
- 若输入合法则输出解压缩后的字符串
- 否则输出字符串!error来报告错误
输入描述
- 输入一行, 为一个 ASCII 字符串
- 长度不超过100字符
- 用例保证输出的字符串长度也不会超过100字符串
输出描述
若判断输入为合法的经过压缩后的字符串, 则输出压缩前的字符串.
若输入不合法, 则输出字符串 !error
.
示例1
输入:
4dff
输出:
ddddff
示例2
输入:
2dff
输出:
!error
示例3
输入:
4d@A
输出:
!error
示例4
输入:
3abb4cd
输出:
aaabbccccd
题解
Python
def main():
# 读取输入
# 遍历字符串s中的所有字符
# 检查输入的字符串s是否只包含数字和小写字母, 如果不是, 就报错并退出
# 遇到数字后, 就将数字后面的字母展开
# 如果最后一个字符是数字, 或者数字后面跟着数字, 也是无效输入
# 如果同一个字母连续超过2次出现, 说明它没有被压缩, 也是无效输入
s = input()
assert 0 < len(s) <= 100
MIN_CONTINUAS_CHARS = 3
is_invalid_input = False
last_number = -1
# 存储所有字符
out_chars = []
# 记录同一个字母连续出现的次数
last_char = ""
last_char_count = 0
# 遍历所有字符
for i in range(len(s)):
char = s[i]
if char.isdigit():
# 数字后面跟着另一个数字
if last_number != -1:
#print("duplicated num:", last_number, char)
is_invalid_input = True
break
last_number = int(char)
# 如果 last_number <= 2, 表示它不应该被压缩
if last_number < MIN_CONTINUAS_CHARS:
is_invalid_input = True
break
# 如果最后一个字符数字, 也是无效输入
if i + 1 == len(s):
is_invalid_input = True
break
# 重置连续字符出现的次数
last_char = ""
last_char_count = 0
elif char.islower():
if last_char == char:
last_char_count += 1
else:
last_char = char
last_char_count = 1
# 先检查同一个字母连续出现的次数, 应该压缩而没有压缩
if last_char_count >= MIN_CONTINUAS_CHARS:
#print("last_char:", last_char, ", last_char_count:", last_char_count)
is_invalid_input = True
break
if last_number == -1:
out_chars.append(char)
else:
# 如果该字母左侧是一个数字, 就把它展开
for i in range(last_number):
out_chars.append(char)
# 并重置上个数字
last_number = -1
else:
# 其它都是无效输入字符
#print("other chars:", char)
is_invalid_input = True
break
if is_invalid_input:
print("!error")
else:
print("".join(out_chars))
if __name__ == "__main__":
main()
智能驾驶
题目描述
有一辆汽车需要从 m * n 的地图左上角起点开往地图的右下角终点, 去往每一个地区都需要消耗一定的油量, 加油站可进行加油.
请你计算汽车确保从从起点到达终点时所需的最少初始油量.
说明:
- 智能汽车可以上下左右四个方向移动
- 地图上的数字取值是 0 或 -1 或 正整数
- -1, 表示加油站, 可以加满油, 汽车的油箱容量最大为100;
- 0: 表示这个地区是障碍物, 汽车不能通过
- 正整数: 表示汽车走过这个地区的耗油量
- 如果汽车无论如何都无法到达终点, 则返回 -1
输入描述
- 第一行为两个数字, M, N, 表示地图的大小为 M * N, 0 < M,N ≤ 200
- 后面一个 M * N 的矩阵, 其中的值是 0 或 -1 或正整数, 加油站的总数不超过 200 个
输出描述
如果汽车无论如何都无法到达终点, 则返回 -1. 如果汽车可以到达终点, 则返回最少的初始油量.
示例1
输入:
2,2
10,20
30,40
输出:
70
说明: 行走的路线为 右→下
示例2
输入:
4,4
10,30,30,20
30,30,-1,10
0,20,20,40
10,-1,30,40
输出:
70
说明: 行走的路线为 右→右→下→下→下→右
示例3
输入:
4,5
10,0,30,-1,10
30,0,20,0,20
10,0,10,0,30
10,-1,30,0,10
输出:
60
说明: 行走的路线为 下→下→下→右→右→上→上→上→右→右→下→下→下
示例4
输入:
4,4
10,30,30,20
30,30,20,10
10,20,10,40
10,20,30,40
输出:
-1
说明: 无论如何都无法到达终点.
题解
字符串拼接
题目描述
构成指定长度字符串的个数.
给定 M (0 < M ≤ 30) 个字符 (a-z) , 从中取出任意字符 (每个字符只能用一次) 拼接成长度为 N (0 < N ≤ 5) 的字符串, 要求相同的字符不能相邻, 计算出给定的字符列表能拼接出多少种满足条件的字符串.
输入非法或者无法拼接出满足条件的字符串则返回0.
输入描述
给定的字符列表和结果字符串长度, 中间使用空格
拼接
输出描述
满足条件的字符串个数.
示例1
输入:
aab 2
输出:
2
说明: 只能构成ab, ba.
示例2
输入:
abc 2
输出:
6
说明: 可以构成 ab ac ba bc ca cb.
题解
项目排期
题目描述
项目组共有N个开发人员, 项目经理接到了M个独立的需求, 每个需求的工作量不同, 且每个需求只能由一个开发人员独立完成, 不能多人合作. 假定各个需求直接无任何先后依赖关系, 请设计算法帮助项目经理进行工作安排, 使整个项目能用最少的时间交付.
输入描述
- 第一行输入为M个需求的工作量, 单位为天, 用逗号隔开
- 例如 X1 X2 X3 … Xm, 表示共有M个需求, 每个需求的工作量分别为X1天, X2天…Xm天
- 其中 0<M<30, 0<Xm<200
- 第二行输入为项目组人员数量N
输出描述
最快完成所有工作的天数.
示例1
输入:
6 2 7 7 9 3 2 1 3 11 4
2
输出:
28
题解
文本统计分析
题目描述
有一个文件, 包含以一定规则写作的文本, 请统计文件中包含的文本数量.
规则如下:
- 文本以
;
分隔, 最后一条可以没有;
, 但空文本不能算语句, 比如COMMAND A; ;
只能算一条语句. 注意, 无字符/空白字符/制表符都算作空
文本 - 文本可以跨行, 比如下面, 是一条文本, 而不是三条:
COMMAND A
AND
COMMAND B;
- 文本支持字符串, 字符串为成对的单引号(')或者成对的双引号(“), 字符串可能出现用转义字符()处理的单双引号
(
"your input is""
) 和转义字符本身, 比如:
COMMAND A "Say \"hello\"";
- 支持注释, 可以出现在字符串之外的任意位置注释以
–
开头, 到换行结束, 比如:
COMMAND A; --this is comment
COMMAND --comment
A AND COMMAND B;
注意字符串内的 –
, 不是注释.
输入描述
文本文件.
输出描述
包含的文本数量.
示例1
输入:
COMMAND TABLE IF EXISTS "UNITED STATE";
COMMAND A GREAT (
ID ADSAB,
download_length INTE-GER, -- test
file_name TEXT,
guid TEXT,
mime_type TEXT,
notifica-tionid INTEGER,
original_file_name TEXT,
pause_reason_type INTEGER,
resumable_flag INTEGER,
start_time INTEGER,
state INTEGER,
folder TEXT,
path TEXT,
total_length INTE-GER,
url TEXT
);
输出:
2
题解
电脑病毒感染
题目描述
一个局域网内有很多台电脑, 分别标注为 0 ~ N-1 的数字. 相连接的电脑距离不一样, 所以感染时间不一样, 感染时间用 t 表示.
其中网络内一台电脑被病毒感染, 求其感染网络内所有的电脑最少需要多长时间. 如果最后有电脑不会感染, 则返回-1.
给定一个数组 times 表示一台电脑把相邻电脑感染所用的时间.
如图: path[i] = {i, j, t}, 表示电脑 i->j, 电脑 i 上的病毒感染 j, 需要时间 t.
输入描述
- 第一行输入一个整数N, 表示局域网内电脑个数 N, 1 ≤ N ≤ 200
- 第二行输入一个整数M, 表示有 M 条网络连接
- 接下来M行, 每行输入为 i, j, t. 表示电脑 i 感染电脑j 需要时间 t. (1 ≤ i, j ≤ N)
- 最后一行为病毒所在的电脑编号
输出描述
输出最少需要多少时间才能感染全部电脑, 如果不存在输出 -1.
示例1
输入:
4
3
2 1 1
2 3 1
3 4 1
2
输出:
2
说明:
- 第一个参数, 局域网内电脑个数N, 1 ≤ N ≤ 200
- 第二个参数, 总共多少条网络连接
- 第三个 2 1 1 表示2->1时间为1
- 第六行, 表示病毒最开始所在电脑号2
题解
周末爬山
题目描述
周末小明准备去爬山锻炼, 0代表平地, 山的高度使用1到9来表示, 小明每次爬山或下山高度只能相差k及k以内, 每次只能上下左右一个方向上移动一格, 小明从左上角(0,0)位置出发.
输入描述
- 第一行输入m n k (空格分隔)
- 代表m*n的二维山地图, k为小明每次爬山或下山高度差的最大值
- 然后接下来输入山地图, 一共m行n列, 均以空格分隔. 取值范围:
- 0 < m ≤ 500
- 0< n ≤ 500
- 0 < k < 5
备注: 所有用例输入均为正确格式, 且在取值范围内, 考生不需要考虑不合法的输入格式.
输出描述
请问小明能爬到的最高峰多高, 到该最高峰的最短步数, 输出以空格分隔.
同高度的山峰输出较短步数.
如果没有可以爬的山峰, 则高度和步数都返回0.
示例1
输入:
5 4 1
0 1 2 0
1 0 0 0
1 0 1 2
1 3 1 0
0 0 0 9
输出:
2 2
说明: 根据山地图可知, 能爬到的最高峰在(0,2)位置, 高度为2, 最短路径为 (0,0)-(0,1)-(0,2), 最短步数为2.
示例2
输入:
5 4 3
0 0 0 0
0 0 0 0
0 9 0 0
0 0 0 0
0 0 0 9
输出:
0 0
说明: 根据山地图可知, 每次爬山距离3, 无法爬到山峰上, 步数为0.
题解
计算网络信号/信号强度
题目描述
网络信号经过传递会逐层衰减, 且遇到阻隔物无法直接穿透, 在此情况下需要计算某个位置的网络信号值. 注意: 网络信号可以绕过阻隔物.
- array[m][n] 的二维数组代表网格地图
- array[i][j] = 0代表i行j列是空旷位置
- array[i][j] = x(x为正整数)代表i行j列是信号源, 信号强度是x
- array[i][j] = -1代表i行j列是阻隔物
- 信号源只有1个, 阻隔物可能有0个或多个
- 网络信号衰减是上下左右相邻的网格衰减1
现要求输出对应位置的网络信号值.
输入描述
输入为三行:
- 第一行为 m, n, 代表输入是一个 m × n 的数组
- 第二行是一串 m × n 个用空格分隔的整数. 每连续 n 个数代表一行, 再往后 n 个代表下一行, 以此类推. 对应的值代表对应的网格是空旷位置, 还是信号源, 还是阻隔物
- 第三行是 i j, 代表需要计算array[i][j]的网络信号值
注意: 此处 i 和 j 均从 0 开始, 即第一行 i 为 0.
6 5
0 0 0 -1 0 0 0 0 0 0 0 0 -1 4 0 0 0 0 0 0 0 0 0 0 -1 0 0 0 0 0
1 4
代表如下地图:
需要输出第1行第4列的网络信号值, 值为2:
输出描述
输出对应位置的网络信号值, 如果网络信号未覆盖到, 也输出0.
一个网格如果可以途径不同的传播衰减路径传达, 取较大的值作为其信号值.
示例1
输入:
6 5
0 0 0 -1 0 0 0 0 0 0 0 0 -1 4 0 0 0 0 0 0 0 0 0 0 -1 0 0 0 0 0
1 4
输出:
2
示例2
输入:
6 5
0 0 0 -1 0 0 0 0 0 0 0 0 -1 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
2 1
输出:
0
题解
九宫格按键输入
题目描述
九宫格按键输入, 输出显示内容, 有英文和数字两个模式, 默认是数字模式, 数字模式直接输出数字, 英文模式连续按同一个按键会依次出现这个按键上的字母, 如果输入”/”或者其他字符, 则循环中断.
字符对应关系如图:
要求输入一串按键, 输出屏幕显示.
- #用于切换模式, 默认是数字模式, 执行#后切换为英文模式
- /表示延迟, 例如在英文模式下, 输入 22/222, 显示为 bc
- 英文模式下, 多次按同一键, 例如输入 22222, 显示为 b
输入描述
输入范围为数字 0~9 和字符 #
, /
, 输出屏幕显示, 例如,
- 在数字模式下, 输入 1234, 显示 1234
- 在英文模式下, 输入 1234, 显示,adg
输出描述
#
用于切换模式, 默认是数字模式, 执行#后切换为英文模式- /表示延迟, 例如在英文模式下, 输入 22/222, 显示为 bc
- 英文模式下, 多次按同一键, 例如输入 22222, 显示为 b
示例1
输入:
2222/22
输出:
222222
说明: 默认数字模式, 字符直接显示, 数字模式下/无序
示例2
输入:
123#222235/56
输出:
123adjjm
说明: 123, #进入英文模式, 连续的数字输入会循环选择字母4个2输出a, 35输出dj56输出jm
示例3
输入:
#2222/22
输出:
ab
说明: #进入英文模式, 连续的数字输入会循环选择字母, 直至输入/, 故第一段2222输入显示a, 第二段22输入显示b.
示例4
输入:
#222233
输出:
ae
说明: #进入英文模式, 连续的数字输入会循环选择字母, 直至输入其他数字, 故第一段2222输入显示a, 第二段33输入显示e.
题解
服务器广播/需要广播的服务器数量
题目描述
服务器连接方式包括直接相连, 间接连接.
A和B直接连接, B和C直接连接, 则A和C间接连接.
直接连接和间接连接都可以发送广播.
给出一个N*N数组, 代表N个服务器.
matrix[i][j] == 1, 则代表i和j直接连接; 不等于 1 时, 代表i和j不直接连接.
matrix[i][i] == 1, 即自己和自己直接连接. matrix[i][j] == matrix[j][i].
计算初始需要给几台服务器广播, 才可以使每个服务器都收到广播.
输入描述
输入为N行, 每行有N个数字, 为0或1, 由空格分隔,
构成N*N的数组, N的范围为 1 <= N <= 40.
输出描述
输出一个数字, 为需要广播的服务器的数量.
示例1
输入:
1 0 0
0 1 0
0 0 1
输出:
3
说明: 3 台服务器互不连接, 所以需要分别广播这 3 台服务器.
示例2
输入:
1 1
1 1
输出:
1
说明: 2 台服务器相互连接, 所以只需要广播其中一台服务器.
题解
参考资料
- "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