简介
数据结构与算法:
数据结构:数据的组织形式,如数组、链表、树、图等,用于高效存储和访问数据
算法:解决问题的步骤,是指令的有限序列,其中每条指令表示一个或多个操作。它具备有穷性(有限步骤内完成)、确定性(每一步都有确切含义)、可行性(能通过有限次操作实现)等基本特性
数据结构为算法提供了操作的对象和基础,而算法则通过操作这些数据结构中的数据来实现特定的功能和性能要求
一、时间 / 空间复杂度
说到算法,就不得不讲讲时间复杂度和 空间复杂度,它们是衡量算法效率的重要指标,下面进行详细介绍
时间复杂度
定义:时间复杂度是指执行算法所需要的计算工作量,通常用大O符号来表示。它描述了算法的运行时间随着输入数据规模的增长而增长的趋势。
常见类型:
- 常数时间复杂度 O(1):算法的执行时间不随输入数据规模的变化而变化,始终保持不变。例如访问数组中的单个元素。
- 对数时间复杂度 O(log n):算法的执行时间随着输入数据规模的对数增长而增长,常见于涉及分治策略的算法,如二分查找。
- 线性时间复杂度 O(n):算法的执行时间与输入数据规模成正比,例如遍历数组的所有元素。
- 线性对数时间复杂度 O(n log n):算法的执行时间与输入数据规模的线性和对数的组合成正比,常见于高效的排序算法,如归并排序、快速排序。
- 平方时间复杂度 O(n²):算法的执行时间与输入数据规模的平方成正比,常见于简单排序算法,如冒泡排序、选择排序。
- 立方时间复杂度 O(n³):算法的执行时间与输入数据规模的立方成正比,较少见,通常出现在三重嵌套循环中。
- 指数时间复杂度 O(2ⁿ):算法的执行时间随着输入数据规模的指数增长而增长,常见于解决组合问题的暴力算法。
- 阶乘时间复杂度 O(n!):算法的执行时间随着输入数据规模的阶乘增长而增长,在解决排列组合问题的暴力算法中出现。
影响因素:
- 算法实现过程:算法的基本操作数量越多,时间复杂度越高。
- 输入数据规模:输入数据的规模越大,时间复杂度越高。
- 算法的复杂度:算法的复杂度越高,时间复杂度越高。
优化策略:
- 优化算法结构:通过改进算法的设计、使用更高效的数据结构、利用算法的特性等,可以减少基本操作的数量,从而降低时间复杂度。
- 选择合适的数据结构:合适的数据结构可以减少算法需要执行的基本操作数量,从而降低时间复杂度。
- 分治法:将问题分割成多个子问题,分别处理,最后合并结果,从而降低时间复杂度。
- 动态规划:将问题分解成多个重叠的子问题,并且将每个子问题的解存储起来,从而避免重复计算,从而降低时间复杂度。
空间复杂度
定义:空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,它也是衡量算法优劣的一个重要方面。
常见类型:
-
常数空间复杂度 O(1):算法所需的存储空间不随输入数据规模的变化而变化,始终保持不变。
-
线性空间复杂度 O(n):算法所需的存储空间与输入数据规模成正比。
-
对数空间复杂度 O(log n):算法所需的存储空间与输入数据规模的对数成正比。
-
指数空间复杂度 O(2ⁿ):算法所需的存储空间随着输入数据规模的指数增长而增长。
影响因素: -
算法实现过程:不同的算法实现方式可能会使用不同数量的额外变量、数据结构等,从而影响空间复杂度。
-
输入数据规模:输入数据量越大,所需的存储空间通常也越大。
-
数据结构的选择:不同的数据结构在存储相同数量的元素时,所需的空间大小可能会有所不同。
综上所述,时间复杂度关注算法执行时间随输入规模的变化趋势,空间复杂度关注算法运行时所需存储空间的大小。它们从不同角度评估算法的效率,对理解和设计高效的算法至关重要。在实际的算法设计和优化中,需要综合考虑时间复杂度和空间复杂度,根据具体的问题场景和需求,选择合适的算法和数据结构,以实现最佳的性能表现。
二、数据结构
JavaScript原生数据结构
1. 数组 (Array)
- 描述:数组是 JavaScript 中最基本的数据结构,可以存储有序的元素。
- 特点:
动态数组:数组长度是动态的,可以增加或删除元素。
支持按索引访问,索引从 0 开始。
数组元素可以是任何类型,包括其他数组。 - 常用方法:
.push(), .pop(), .shift(), .unshift(), .map(), .filter(), .reduce()
2. 对象 (Object)
- 描述:对象是无序的键值对集合。
- 特点:
键(属性名)是字符串(或 Symbol),值可以是任何数据类型。
支持通过键(属性名)进行访问。 - 常用方法:
.keys(), .values(), .entries(), .hasOwnProperty()
3. Set
- 描述:Set 是一种集合类型的数据结构,包含唯一的值。
- 特点:
不允许重复的元素。
存储的值是无序的。 - 常用方法:
.add(), .delete(), .has(), .clear(), .size
4. Map
- 描述:Map 是一种键值对的集合,键可以是任何数据类型,而不仅仅是字符串。
- 特点:
允许任何类型的键(如对象、数组等),并且保证插入顺序。
支持通过键进行查找。 - 常用方法:
.set(), .get(), .delete(), .has(), .clear(), .size
5. WeakSet
- 描述:WeakSet 是一种类似于 Set 的集合类型,存储的是对象的弱引用。
- 特点:
存储的元素只能是对象,且这些对象的引用是弱引用。
垃圾回收机制允许 WeakSet 中的对象在不再被引用时自动被清理。 - 常用方法:
.add(), .delete(), .has()
6. WeakMap
- 描述:WeakMap 是一种类似于 Map 的集合类型,存储的是键值对,其中键是对象,值可以是任意数据类型。
- 特点:
键必须是对象,且键的引用是弱引用。
垃圾回收机制允许 WeakMap 中的键在没有其他引用时自动被清理。 - 常用方法:
.set(), .get(), .delete(), .has()
除了上述JavaScript原生具备的数据结构,还有一些常见的数据结构在JavaScript中并不直接存在,但我们可以利用其基础数据结构(例如数组、对象、Set以及Map等)来对它们进行模拟。
下面将介绍一些不存在于JavaScript中但能够通过基础数据结构进行:
JavaScrip模拟实现的数据结构
1. 栈 (Stack)
- 描述:栈是一种后进先出(LIFO, Last In First Out)的数据结构。元素从一端(栈顶)进行插入和删除。
- 模拟方式:可以使用 数组 来模拟栈,通过 push() 方法插入元素,pop() 方法删除元素。
let stack = [];
stack.push(1); // 入栈
stack.push(2);
stack.pop(); // 出栈,返回 2
2. 队列 (Queue)
- 描述:队列是一种先进先出(FIFO, First In First Out)的数据结构。元素从一端(队尾)插入,从另一端(队头)删除。
- 模拟方式:可以使用 数组 来模拟队列,通过 push() 方法插入元素,shift() 方法删除元素(虽然 shift() 的时间复杂度是 O(n),但可以作为一种简单模拟)。
let queue = [];
queue.push(1); // 入队
queue.push(2);
queue.shift(); // 出队,返回 1
也可以使用 LinkedList 或者 Deque 来模拟队列,以优化性能。
3. 链表 (Linked List)
- 描述:链表是一种线性数据结构,由一系列节点构成,每个节点包含数据和指向下一个节点的引用。
- 模拟方式:可以通过 对象 来模拟链表的节点,每个节点包含数据和指向下一个节点的引用。
let node1 = { value: 1, next: null };
let node2 = { value: 2, next: null };
node1.next = node2; // node1 指向 node2
可以通过链表节点实现常见操作,如插入、删除、查找等。
4. 哈希表 (Hash Table)
- 描述:哈希表是一种键值对数据结构,通过哈希函数将键映射到对应的值。哈希表允许高效的查找、插入和删除操作。
- 模拟方式:可以使用 对象 或 Map 来模拟哈希表,键是对象的属性或 Map 的键,值可以是任何数据类型。
let hashTable = {};
hashTable['key1'] = 'value1';
hashTable['key2'] = 'value2';
console.log(hashTable['key1']); // 输出 'value1'
也可以使用 Map,它提供了更灵活的键类型。
5. 优先队列 (Priority Queue)
- 描述:优先队列是一个每次插入时根据优先级进行排序的队列,优先级较高的元素会先被移除。
- 模拟方式:可以使用 数组 来模拟优先队列,使用 sort() 方法保持队列按优先级排序。插入新元素时按优先级进行排序,删除时从数组的头部弹出元素。
let pq = [];
pq.push({ value: 'task1', priority: 2 });
pq.push({ value: 'task2', priority: 1 });
pq.sort((a, b) => a.priority - b.priority); // 按优先级排序
pq.shift(); // 取出优先级最高的元素
这种方法的时间复杂度较高,但在简单情况下是有效的。
6. 双端队列 (Deque)
- 描述:双端队列允许从队列的两端进行插入和删除。
- 模拟方式:可以使用 数组 的 push() 和 shift() 方法(对于队尾和队头),从而模拟双端队列。
let deque = [];
deque.push(1); // 从尾部插入
deque.unshift(2); // 从头部插入
deque.pop(); // 从尾部删除
deque.shift(); // 从头部删除
7. 图 (Graph)
- 描述:图是一种由节点(顶点)和节点之间的边构成的数据结构。图可以是有向图或无向图。
- 模拟方式:可以通过 对象 或 Map 来模拟图,其中每个节点可以作为键,邻接节点列表(数组)作为值。
let graph = {};
graph['A'] = ['B', 'C'];
graph['B'] = ['A', 'D'];
graph['C'] = ['A', 'D'];
graph['D'] = ['B', 'C'];
可以通过邻接表或邻接矩阵的方式来存储图的数据。
8. 树 (Tree)
- 描述:树是一种层次化的数据结构,由节点和边组成,每个节点可以有零个或多个子节点。
- 模拟方式:可以通过 对象 或 类 来模拟树结构,其中每个节点可以包含值和指向子节点的引用。
class TreeNode {
constructor(value) {
this.value = value;
this.children = [];
}
}
let root = new TreeNode(1);
let child1 = new TreeNode(2);
let child2 = new TreeNode(3);
root.children.push(child1, child2);
9. 集合 (Set)
- 描述:集合是一种不允许重复元素的数据结构,可以用来测试元素是否存在。
- 模拟方式:虽然 JavaScript 原生提供了 Set 类型,但如果要模拟它,可以使用 对象 来实现,使用对象的键来表示集合元素。
let set = {};
set['a'] = true; // 模拟元素 'a'
set['b'] = true; // 模拟元素 'b'
10. 堆 (Heap)
- 描述:堆是一种完全二叉树数据结构,通常用于实现优先队列。堆有两种类型:最大堆和最小堆。
- 模拟方式:可以使用 数组 来模拟堆,按特定规则(如最大堆或最小堆)维护数组的顺序。
– 插入元素时,将新元素添加到数组末尾,然后进行 “上浮” 操作。
– 删除最大/最小元素时,交换根节点和最后一个元素,然后进行 “下沉” 操作。
三、算法
排序算法 (Sorting Algorithms)
排序算法用于将一组元素按照指定顺序(升序或降序)排列。
1. 冒泡排序
思路:比较所有相邻元素,如果第一个比第二个大,则交换;一轮下来,可以保证最后一个数是最大的
执行 n - 1 轮,就可以完成排序
时间复杂度:使用两个嵌套循环,时间复杂度为 O(n^2)
空间复杂度:原地排序算法,交换位置时不需要额外的存储空间,空间复杂度为O(1)
const arr = [5,4,2,3,20,8,2,17,55,86,5]
// 冒泡排序
function bubbleSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - i; j++) {
if (arr[j] > arr[j + 1]) {
let temp = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = temp
}
}
}
return arr
}
console.log(bubbleSort(arr)) // [2,2,3,4,5,5,8,17,20,55,86]
2. 快速排序
思路:
- 分区:从数组中任意选择一个“基准”,所有比基准小的元素放在基准前面,比基准大的元素放在基准的后面
- 递归:使用递归的方式对基准前后的子数组进行分区
- 退出条件:当子数组的长度小于等于1时结束递归
时间复杂度:分区的时间复杂度是 O(n),递归的时间复杂度是 O(logN),所以快速排序的时间复杂度为 O(n * logN)
空间复杂度:取决于递归调用的深度,最坏的情况下递归的深度可能会达到n,导致空间复杂度变为 O(n)
const arr = [5,4,2,3,20,8,2,17,55,86,5]
// 快速排序
function quickSort(arr) {
if (arr.length <= 1) return arr
let left = []
let right = []
let mid = arr[0]
for (let i = 1; i < arr.length; i++) {
if (arr[i] < mid) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
left = quickSort(left)
right = quickSort(right)
left.push(mid)
return left.concat(right)
}
console.log(quickSort(arr)) // [2,2,3,4,5,5,8,17,20,55,86]
3. 选择排序
思路:每次选择最小(或最大)元素放到已排序部分的末尾。
时间复杂度:使用两个嵌套循环,时间复杂度为 O(n^2)
空间复杂度:原地排序算法,交换位置时不需要额外的存储空间,空间复杂度为O(1)
const arr = [5,4,2,3,20,8,2,17,55,86,5]
function selectionSort(arr) {
let n = arr.length;
// 外层循环遍历数组
for (let i = 0; i < n - 1; i++) {
let minIndex = i; // 假设当前元素是最小值
// 内层循环查找未排序部分的最小元素
for (let j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j; // 记录最小值的位置
}
}
// 如果找到更小的元素,则交换
if (minIndex !== i) {
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]; // 交换元素
}
}
return arr;
}
console.log(selectionSort(arr)); // [2,2,3,4,5,5,8,17,20,55,86]
4. 插入排序
思路:通过将未排序的元素插入已排序部分来构建最终有序数组。
5. 归并排序
思路:通过分治法递归地将数组分成两部分并排序。
6. 堆排序
思路:利用堆这种数据结构对数组进行排序。
查找算法 (Searching Algorithms)
查找算法用于在数据集中寻找特定的元素。
- 线性查找 (Linear Search):逐个元素检查直到找到目标元素。
- 二分查找 (Binary Search):通过将数据集分成两半,不断缩小查找范围来快速找到目标元素(前提是数据必须是已排序的)。
递归算法 (Recursion Algorithms)
递归是算法中一种通过调用自身解决问题的方式。
- 阶乘计算:例如 factorial(n) 计算 n!。
- 斐波那契数列:递归实现 Fibonacci 数列。
- 二叉树遍历:如先序遍历、后序遍历、层序遍历等。
动态规划 (Dynamic Programming)
动态规划用于分解问题并存储子问题的解以避免重复计算。
- 背包问题 (Knapsack Problem):解决在给定容量限制的情况下如何选择物品使得总价值最大。
- 最长公共子序列 (Longest Common Subsequence, LCS):求解两个字符串的最长公共子序列。
- 编辑距离 (Edit Distance):计算将一个字符串转换为另一个字符串所需的最少操作数。
贪心算法 (Greedy Algorithms)
贪心算法通过每次选择当前最优解来寻找全局最优解。
- 活动选择问题:选择不冲突的活动集合以最大化活动的数量。
- 霍夫曼编码 (Huffman Coding):用于数据压缩的贪心算法。
图算法 (Graph Algorithms)
图算法用于处理图数据结构,如节点和边的集合。
- 广度优先搜索 (BFS):从起始节点开始,层层推进,访问每个邻接节点。
- 深度优先搜索 (DFS):从起始节点开始,一直深入到没有未访问邻接节点的节点。
- Dijkstra 算法:用于找出加权图中最短路径。
- Floyd-Warshall 算法:用于求解所有节点之间的最短路径。
字符串算法 (String Algorithms)
字符串算法主要处理字符串的匹配、查找和比较等操作。
- KMP 算法 (Knuth-Morris-Pratt):高效的字符串匹配算法,用于在文本中查找子字符串。
- Rabinkarp 算法:另一种字符串匹配算法,通过哈希值来加速匹配过程。
分治算法 (Divide and Conquer)
分治算法通过将问题分解成更小的子问题来解决。
- 归并排序 (Merge Sort):通过递归拆分数组并合并来排序。
- 快速排序 (Quick Sort):通过递归将数组分区并排序。
位运算算法 (Bitwise Algorithms)
位运算常用于低级别的优化和数学操作。
- 位运算加法:通过位运算实现加法,通常不使用算术运算符。
- 快速幂 (Fast Exponentiation):通过位运算和递归快速计算幂。
- 汉明距离 (Hamming Distance):计算两个数的二进制表示中不同位的数量。
回溯算法 (Backtracking)
回溯算法通过逐步构建解空间树,并在发现无解时返回上一层尝试其他可能。
- N皇后问题 (N-Queens Problem):将N个皇后放置在N×N棋盘上,使得它们互不攻击。
- 数独问题 (Sudoku Solver):通过回溯法解决数独问题。
排序与查找的高级算法
- 桶排序 (Bucket Sort):通过将元素分到多个桶中,并分别对桶内的元素排序来实现排序。
- 计数排序 (Counting Sort):利用输入元素的值范围来优化排序过程。
并查集 (Union-Find)
并查集是一种数据结构,用于处理不相交集合的合并和查询问题,通常用于图的连通性判断。
- 动态连通性问题:判断两个元素是否属于同一集合。