参考引用
以上每一步都采取当前看来最好的选择(尽可能用大面额的货币),最终得到了可行的找零方案,这种方法本质上是 “贪心” 算法
数据结构(data structure)是计算机中组织和存储数据的方式,具有以下设计目标
数据结构设计是一个充满权衡的过程。如果想要在某方面取得提升,往往需要在另一方面作出妥协,如:
数据结构与算法是独立于编程语言的
逻辑结构揭示了数据元素之间的逻辑关系
如下图所示,逻辑结构可被分为 “线性” 和 “非线性” 两大类。线性结构比较直观,指数据在逻辑关系上呈线性排列;非线性结构则相反,呈非线性排列
非线性数据结构可以进一步被划分为树形结构和网状结构
- 线性结构:数组、链表、队列、栈、哈希表,元素之间是一对一的顺序关系
- 树形结构:树、堆、哈希表,元素之间是一对多的关系
- 网状结构:图,元素之间是多对多的关系
在计算机中,内存和硬盘是两种主要的存储硬件设备
在算法运行过程中,相关数据都存储在内存中
系统通过内存地址来访问目标位置的数据
基本数据类型是 CPU 可以直接进行运算的类型,在算法中直接被使用,主要包括以下几种类型
基本数据类型以二进制的形式存储在计算机中
在能够解决问题的前提下,算法效率是衡量算法优劣的主要评价指标,它包括以下两个维度
效率评估方法主要分为两种:实际测试、理论估算
可以考虑仅通过一些计算来评估算法的效率。这种估算方法被称为:渐近复杂度分析 asymptotic complexity analysis,简称复杂度分析
复杂度分析体现算法运行所需的时间(空间)资源与输入数据大小之间的关系。它描述了随着输入数据大小的增加,算法执行所需时间和空间的增长趋势
int forLoop(int n) {
int res = 0;
for (int i = 1; i <= n; ++i) {
res += i;
}
return res;
}
/* while 循环(两次更新) */
int whileLoopII(int n) {
int res = 0;
int i = 1; // 初始化条件变量
// 循环求和 1, 4, ...
while (i <= n) {
res += i;
// 更新条件变量
i++;
i *= 2;
}
return res;
}
/* 双层 for 循环 */
string nestedForLoop(int n) {
ostringstream res;
// 循环 i = 1, 2, ..., n-1, n
for (int i = 1; i <= n; ++i) {
// 循环 j = 1, 2, ..., n-1, n
for (int j = 1; j <= n; ++j) {
res << "(" << i << ", " << j << "), ";
}
}
return res.str();
}
递归(recursion)是一种算法策略,通过函数调用自身来解决问题,它主要包含两个阶段
从实现的角度看,递归代码主要包含三个要素
只需调用函数 recur(n) ,就可以完成 1 + 2 + … + n 的计算
int recur(int n) {
// 终止条件
if (n == 1)
return 1;
// 递:递归调用
int res = recur(n - 1);
// 归:返回结果
return n + res;
}
int tailRecur(int n, int res) {
// 终止条件
if (n == 0)
return res;
// 尾递归调用
return tailRecur(n - 1, res + n);
}
int fib(int n) {
// 终止条件 f(1) = 0, f(2) = 1
if (n == 1 || n == 2)
return n - 1;
// 递归调用 f(n) = f(n-1) + f(n-2)
int res = fib(n - 1) + fib(n - 2);
// 返回结果 f(n)
return res;
}
// 算法 A 的时间复杂度:常数阶
void algorithm_A(int n) {
cout << 0 << endl;
}
// 算法 B 的时间复杂度:线性阶
void algorithm_B(int n) {
for (int i = 0; i < n; i++) {
cout << 0 << endl;
}
}
// 算法 C 的时间复杂度:常数阶
void algorithm_C(int n) {
for (int i = 0; i < 1000000; i++) {
cout << 0 << endl;
}
}
给定一个输入大小为 n 的函数
void algorithm(int n) {
int a = 1; // +1
a = a + 1; // +1
a = a * 2; // +1
// 循环 n 次
for (int i = 0; i < n; i++) { // +1(每轮都执行 i++)
cout << 0 << endl; // +1
}
}
设算法的操作数量是一个关于输入数据大小 n 的函数,记为 T(n),则以上函数的的操作数量为:T(n) = 3 + 2n
计算渐近上界就是寻找一个函数 f(n),使得当 n 趋向于无穷大时,T(n) 和 f(n) 处于相同的增长级别,仅相差一个常数项 c 的倍数
// 时间复杂度:O(n^2)
void algorithm(int n) {
int a = 1; // +0(技巧 1)
a = a + n; // +0(技巧 1)
// +n(技巧 2)
for (int i = 0; i < 5 * n + 1; i++) {
cout << 0 << endl;
}
// +n*n(技巧 3)
for (int i = 0; i < 2 * n; i++) {
for (int j = 0; j < n + 1; j++) {
cout << 0 << endl;
}
}
}
int constant(int n) {
int count = 0;
int size = 100000;
for (int i = 0; i < size; i++) {
count++;
}
return count;
}
线性阶的操作数量相对于输入数据大小 n 以线性级别增长,通常出现在单层循环中
int linear(int n) {
int count = 0;
for (int i = 0; i < n; i++) {
count++;
}
return count;
}
遍历数组和遍历链表等操作的时间复杂度均为 O(n) ,其中 n 为数组或链表的长度
int arrayTraversal(vector &nums) {
int count = 0;
// 循环次数与数组长度成正比
for (int num : nums) {
count++;
}
return count;
}
int quadratic(int n) {
int count = 0;
// 循环次数与数组长度成平方关系
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
count++;
}
}
return count;
}
/* 指数阶(循环实现) */
int exponential(int n) {
int count = 0, base = 1;
// 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)
for (int i = 0; i < n; i++) {
for (int j = 0; j < base; j++) {
count++;
}
base *= 2;
}
// count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
return count;
}
/* 指数阶(递归实现) */
int expRecur(int n) {
if (n == 1)
return 1;
return expRecur(n - 1) + expRecur(n - 1) + 1;
}
指数阶增长非常迅速,在穷举法(暴力搜索、回溯等)中比较常见。对于数据规模较大的问题,指数阶是不可接受的,通常需要使用动态规划或贪心等算法来解决
/* 对数阶(循环实现) */
int logarithmic(float n) {
int count = 0;
while (n > 1) {
n = n / 2;
count++;
}
return count;
}
/* 对数阶(递归实现) */
int logRecur(float n) {
if (n <= 1)
return 0;
return logRecur(n / 2) + 1;
}
对数阶常出现于基于分治策略的算法中,体现了 “一分为多” 和 “化繁为简” 的算法思想。它增长缓慢,是仅次于常数阶的理想的时间复杂度
线性对数阶常出现于嵌套循环中,两层循环的时间复杂度分别为 O(log n) 和 O(n)
int linearLogRecur(float n) {
if (n <= 1)
return 1;
int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);
for (int i = 0; i < n; i++) {
count++;
}
return count;
}
二叉树的每一层操作总数都为 n ,树共有 log_2 n + 1 层,因此时间复杂度为 O(n log n)
主流排序算法的时间复杂度通常为 O(n log n),例如快速排序、归并排序、堆排序等
/* 阶乘阶(递归实现) */
int factorialRecur(int n) {
if (n == 0)
return 1;
int count = 0;
// 从 1 个分裂出 n 个
for (int i = 0; i < n; i++) {
count += factorialRecur(n - 1);
}
return count;
}
阶乘阶比指数阶增长得更快,在 n 较大时也是不可接受的
算法在运行过程中使用的内存空间主要包括以下几种
一般情况下,空间复杂度的统计范围是 “暂存空间” 加上 “输出空间”
暂存空间可以进一步划分为三个部分
在分析一段程序的空间复杂度时,通常统计暂存数据、栈帧空间和输出数据三部分
struct Node {
int val;
Node *next;
Node(int x) : val(x), next(nullptr) {}
};
int func() {
// 执行某些操作...
return 0;
}
int algorithm(int n) { // 输入数据
const int a = 0; // 暂存数据(常量)
int b = 0; // 暂存数据(变量)
Node* node = new Node(0); // 暂存数据(对象)
int c = func(); // 栈帧空间(调用函数)
return a + b + c; // 输出数据
}
通常只关注最差空间复杂度,因为内存空间是一项硬性要求,必须确保在所有输入数据下都有足够的内存空间预留
在递归函数中,需要注意统计栈帧空间
int func() {
// 执行某些操作
return 0;
}
/* 循环 O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
func();
}
}
/* 递归 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
}
int func() {
// 执行某些操作
return 0;
}
void constant(int n) {
// 常量、变量、对象占用 O(1) 空间
const int a = 0;
int b = 0;
vector<int> nums(10000);
ListNode node(0);
// 循环中的变量占用 O(1) 空间
for (int i = 0; i < n; i++) {
int c = 0;
}
// 循环中的函数占用 O(1) 空间
for (int i = 0; i < n; i++) {
func();
}
}
void linear(int n) {
// 长度为 n 的数组占用 O(n) 空间
vector<int> nums(n);
// 长度为 n 的列表占用 O(n) 空间
vector<ListNode> nodes;
for (int i = 0; i < n; i++) {
nodes.push_back(ListNode(i));
}
// 长度为 n 的哈希表占用 O(n) 空间
unordered_map<int, string> map;
for (int i = 0; i < n; i++) {
map[i] = to_string(i);
}
}
/* 线性阶(递归实现) */
void linearRecur(int n) {
cout << "递归 n = " << n << endl;
if (n == 1)
return;
linearRecur(n - 1);
}
void quadratic(int n) {
// 二维列表占用 O(n^2) 空间
vector<vector<int>> numMatrix;
for (int i = 0; i < n; i++) {
vector<int> tmp;
for (int j = 0; j < n; j++) {
tmp.push_back(0);
}
numMatrix.push_back(tmp);
}
}
/* 平方阶(递归实现) */
int quadraticRecur(int n) {
if (n <= 0)
return 0;
vector<int> nums(n);
cout << "递归 n = " << n << " 中的 nums 长度 = " << nums.size() << endl;
return quadraticRecur(n - 1);
}
/* 指数阶(建立满二叉树) */
TreeNode *buildTree(int n) {
if (n == 0)
return nullptr;
TreeNode *root = new TreeNode(0);
root->left = buildTree(n - 1);
root->right = buildTree(n - 1);
return root;
}