算法与数据结构是计算机科学的核心支柱,它们共同决定了程序的效率和性能。算法提供解决问题的步骤,而数据结构则是组织和管理数据的方式。
什么是算法?
算法(Algorithm)是一组明确定义的指令,用于在有限时间内解决特定问题或完成特定任务。算法如同菜谱,指导计算机如何一步步完成计算。例如,排序算法可以把一组杂乱的数字按顺序排列。
算法的核心特性包括:
- 输入:算法接收一个或多个输入。
- 输出:算法产生一个或多个输出。
- 确定性:每一步操作都明确且无歧义。
- 有限性:算法在有限步骤内结束。
- 有效性:每一步都能被精确执行。
时间复杂度
时间复杂度是衡量算法效率的重要指标,表示算法运行所需时间随输入规模增长的趋势。时间复杂度通常用大O记号(Big-O Notation)表示,描述最坏情况下的时间增长。
常见的时间复杂度
- O(1) (常数时间):无论输入规模如何,运行时间固定。例如,访问数组中的某个元素。
- O(n) (线性时间):运行时间与输入规模成正比。例如,遍历一个长度为n的数组。
- O(n²) (平方时间):运行时间与输入规模的平方成正比。例如,嵌套循环遍历二维矩阵。
- O(log n) (对数时间):运行时间随输入规模的对数增长。例如,二分查找。
- O(n log n) :常见于高效排序算法,如快速排序或归并排序。
- O(2ⁿ) (指数时间):运行时间随输入规模呈指数增长,常见于某些递归算法。
如何分析时间复杂度?
- 识别基本操作:找出算法中最频繁执行的操作(如比较、赋值)。
- 计算操作次数:根据输入规模n,估算基本操作的执行次数。
- 提取主导项:忽略常数和低阶项,用大O记号表示。
例如,一个循环遍历n个元素的时间复杂度是O(n),而双重循环则是O(n²)。
TypeScript 示例展示时间复杂度
以下是通过简单 TypeScript 代码展示不同时间复杂度的示例:
- O(1) 示例:访问数组第一个元素
function constantTime(arr: number[]): number {
return arr[0]; // 无论数组大小,都只需一步操作
}
- O(n) 示例:求数组和
function linearTime(arr: number[]): number {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i]; // 循环 n 次
}
return sum;
}
- O(n²) 示例:简单冒泡排序
function quadraticTime(arr: number[]): number[] {
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length; j++) {
if (arr[i] < arr[j]) {
[arr[i], arr[j]] = [arr[j], arr[i]]; // 嵌套循环,n² 次操作
}
}
}
return arr;
}
- O(log n) 示例:二分查找(假设数组已排序)
function logTime(arr: number[], target: number): number {
let low = 0;
let high = arr.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] < target) low = mid + 1;
else high = mid - 1;
}
return -1; // 每次循环问题规模减半
}
空间复杂度
空间复杂度衡量算法运行时所需的额外内存空间,也用大O记号表示。它包括:
- 固定部分:如变量、常数等,与输入规模无关。
- 可变部分:如动态分配的数组、递归调用栈等,随输入规模变化。
常见空间复杂度
- O(1) :只使用固定数量的变量。例如,交换两个变量。
- O(n) :使用与输入规模成正比的额外空间。例如,复制一个长度为n的数组。
- O(n²) :使用二维数组等。例如,存储一个n×n的矩阵。
- O(log n) :常见于递归算法的调用栈,如二分查找。
优化空间复杂度
- 原地算法:尽量在原数据结构上操作,减少额外空间。例如,快速排序是原地排序算法。
- 复用空间:通过复用已分配的内存减少开销。
TypeScript 示例展示空间复杂度
- O(1) 示例:交换两个变量。
function swap(a: number, b: number): [number, number] {
let temp = a;
a = b;
b = temp;
return [a, b]; // 只用固定空间
}
- O(n) 示例:复制数组。
function copyArray(arr: number[]): number[] {
const copy = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
copy[i] = arr[i];
}
return copy; // 额外 n 个空间
}
- O(n²) 示例:创建 n x n 矩阵。
function createMatrix(n: number): number[][] {
const matrix: number[][] = [];
for (let i = 0; i < n; i++) {
matrix[i] = new Array(n).fill(0); // n² 空间
}
return matrix;
}
算法设计的基本范式
学习算法时,了解常见的算法设计范式有助于快速解决问题:
- 分治法(Divide and Conquer):将问题分解为小问题,解决后再合并。如归并排序。
- 贪心算法(Greedy Algorithm):每一步选择局部最优解,期望得到全局最优。如最小生成树算法。
- 动态规划(Dynamic Programming):通过记录子问题结果避免重复计算。如背包问题。
- 回溯法(Backtracking):尝试所有可能解,逐步排除无效解。如八皇后问题。
TypeScript 示例展示算法范式
- 分治法示例:归并排序
function mergeSort(arr: number[]): number[] {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right); // 分治 + 合并
}
function merge(left: number[], right: number[]): number[] {
const result: number[] = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (left[i] < right[j]) result.push(left[i++]);
else result.push(right[j++]);
}
return result.concat(left.slice(i)).concat(right.slice(j));
}
数据结构基础
数据结构是组织和管理数据的方式,与算法密切相关。算法往往依赖合适的数据结构来实现高效操作。以下是常见数据结构的基础概念:
- 数组(Array) :连续内存存储的元素集合,支持随机访问。时间复杂度:访问 O(1),插入/删除 O(n)。
- 链表(Linked List) :由节点组成的链式结构,每个节点包含数据和指向下一个节点的指针。时间复杂度:访问 O(n),插入/删除 O(1)(若已知位置)。
- 栈(Stack) :后进先出(LIFO)的结构,支持 push 和 pop 操作。常用于递归、表达式求值。
- 队列(Queue) :先进先出(FIFO)的结构,支持 enqueue 和 dequeue 操作。常用于 BFS(广度优先搜索)。
- 树(Tree) :层次结构,通常有根节点、子节点。二叉树是最简单形式,每个节点最多两个子节点。时间复杂度:搜索 O(log n)(平衡二叉树)。
- 图(Graph) :由节点(顶点)和边组成的结构,可表示网络关系。支持有向/无向、加权/非加权。常见算法:DFS、BFS、最短路径。
TypeScript 示例展示数据结构
- 数组示例:
const arr: number[] = [1, 2, 3];
console.log(arr[1]); // 访问 O(1)
- 链表示例:
class Node {
value: number;
next: Node | null = null;
constructor(value: number) {
this.value = value;
}
}
const head = new Node(1);
head.next = new Node(2); // 插入 O(1)
- 栈示例:
const stack: number[] = [];
stack.push(1); // 入栈
const top = stack.pop(); // 出栈
总结
算法与数据结构是计算机科学的基础,时间复杂度和空间复杂度是衡量算法效率的核心指标,数据结构则是算法高效实现的关键。