随着前端技术的不断发展,对于前端工程师的技能要求也越来越高。掌握基础算法和数据结构已经成为前端开发的必备技能。
最近有一些小伙伴面试的时候,经常会被问到一些算法相关的题和一些数据结构的知识,因此这篇文章主要讲解一下目前前端需要掌握的一些算法和数据结构相关的知识点。
算法
排序算法
1. 冒泡排序:
应用场景:适用于数据量不大的简单排序需求。
算法面试题:给定一个数组,使用冒泡排序对其进行排序。
js
function bubbleSort(arr) {
let len = arr.length;
for (let i = 0; i < len - 1; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
2. 快速排序:
应用场景:适用于大数据量的排序,是目前使用最广泛的排序算法之一。
算法面试题:给定一个数组,使用快速排序对其进行排序。
js
function quickSort(arr) {
if (arr.length <= 1) return arr;
const pivotIndex = Math.floor(arr.length / 2);
const pivot = arr.splice(pivotIndex, 1)[0];
const left = [];
const right = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat([pivot], quickSort(right));
}
3. 插入排序:
应用场景:插入排序适合数据量较小,或者数据基本有序的场景。
算法面试题:给定一个数组,使用插入排序对其进行排序。
js
function insertionSort(arr) {
let n = arr.length;
for (let i = 1; i < n; i++) {
let value = arr[i];
let j = i - 1;
while (j >= 0 && arr[j] > value) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = value;
}
return arr;
}
4. 选择排序:
应用场景:选择排序适用于数据量不大的场景。每次选择最小的元素放到已排序序列的末尾,该算法不稳定,性能略优于冒泡排序。
算法面试题:给定一个数组,使用选择排序对其进行排序。
js
function selectionSort(arr) {
let n = arr.length;
for (let i = 0; i < n; 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;
}
5. 选择排序:
应用场景:归并排序适用于大数据量的排序,尤其是当数据结构被限制为无法随意访问,比如链表。优势在于稳定性好,分治策略使得其在数据量大时仍能保持很好的性能表现。
算法面试题:给定一个数组,使用归并排序对其进行排序。
js
function mergeSort(arr) {
if (arr.length <= 1) return arr;
const middle = Math.floor(arr.length / 2);
const left = arr.slice(0, middle);
const right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right) {
let result = [], leftIndex = 0, rightIndex = 0;
while (leftIndex < left.length && rightIndex < right.length) {
if (left[leftIndex] < right[rightIndex]) {
result.push(left[leftIndex]);
leftIndex++;
} else {
result.push(right[rightIndex]);
rightIndex++;
}
}
return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}
搜索算法
1. 线性搜索:
应用场景:当数据无序或者数据量较小,且没有额外空间来进行排序时,线性搜索是一个简单直接的选择。
算法面试题:在一个无序数组中找出某个特定值的索引,如果不存在则返回-1。
js
function linearSearch(arr, target) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) return i;
}
return -1;
}
2. 线性搜索:
应用场景:二分搜索适用于有序数组的查找操作,当数据规模较大时,二分搜索的效率远高于线性搜索。
算法面试题:在一个有序数组中找出某个特定值的索引,如果不存在则返回-1。
js
function binarySearch(arr, target) {
let low = 0;
let high = arr.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const guess = arr[mid];
if (guess === target) return mid;
if (guess > target) high = mid - 1;
else low = mid + 1;
}
return -1;
}
遍历算法
1. 深度优先遍历:
应用场景:一般用在需要访问或搜索树、图中所有节点,或者找到从根节点到目标节点路径的问题解决方案,在前端中常用于DOM的查询和处理。
算法面试题:给定一个树状结构,利用DFS算法遍历所有节点。
js
function depthFirstSearch(root) {
const stack = [root];
while (stack.length > 0) {
const currentNode = stack.pop();
console.log(currentNode.value); // 处理当前节点
if (currentNode.children) {
for (let i = currentNode.children.length - 1; i >= 0; i--) {
stack.push(currentNode.children[i]);
}
}
}
}
2. 广度优先遍历:
应用场景:适用于层级结构的数据遍历,如社交网络、树结构等,尤其是距离起点近的节点先被探索。
算法面试题:给定一个树状结构,利用BFS算法遍历所有节点。
js
function breadthFirstSearch(root) {
const queue = [root];
while (queue.length > 0) {
const currentNode = queue.shift();
console.log(currentNode.value); // 处理当前节点
if (currentNode.children) {
for (const child of currentNode.children) {
queue.push(child);
}
}
}
}
字符串匹配算法
1. KMP算法:
应用场景:适合于长文本字符串的子串搜索,性能优于简单的串口匹配算法。
算法面试题:给定一个主文本和一个模式串,使用KMP算法寻找模式串在主文本中的位置。
js
// KMP算法的处理部分比较复杂,因此在这里简化演示。
// 已知 next 数组的构造方法,然后根据next数组进行匹配
function kmpSearch(text, pattern) {
let i = 0; // text的位置
let j = 0; // pattern的位置
let next = getNext(pattern); // 构造next数组
while (i < text.length && j < pattern.length) {
if (j === -1 || text[i] === pattern[j]) {
i++;
j++;
} else {
j = next[j];
}
}
if (j === pattern.length) return i - j; // 匹配成功
return -1; // 匹配失败
}
// next数组构造
function getNext(pattern) {
// ...代码逻辑...
// 这里就不写了,免得你们说我水字数
}
动态规划:
应用场景:动态规划适用于解决具有重叠子问题和最优子结构性质的问题,例如斐波那契数列、最短路径问题等;在前端领域,可能会用来解决某些复杂的状态管理问题,或者计算最优解。
算法面试题:给定一个正整数数组,找出数组中元素相加等于给定目标值的组合数量。
js
function combinationSum4(nums, target) {
const dp = new Array(target + 1).fill(0);
dp[0] = 1;
for (let i = 1; i <= target; i++) {
for (const num of nums) {
if (i >= num) {
dp[i] += dp[i - num];
}
}
}
return dp[target];
}
贪心算法:
应用场景:贪心算法适合解决能够将问题分成多个部分,每一部分都可以找到局部最优解,并且最终局部最优解的组合能够达到全局最优解的问题,常见的如找零钱问题、最小生成树问题等。
算法面试题:给定一系列非负整数代表升高的墙,计算下雨后能接多少水。
js
function trap(height) {
if (height === null || height.length === 0) return 0;
let sum = 0, maxLeft = 0, maxRight = 0;
let left = 0, right = height.length - 1;
while (left < right) {
if (height[left] <= height[right]) {
maxLeft = Math.max(maxLeft, height[left]);
sum += maxLeft - height[left];
left++;
} else {
maxRight = Math.max(maxRight, height[right]);
sum += maxRight - height[right];
right--;
}
}
return sum;
}
回溯算法:
应用场景:回溯算法非常适合解决决策树的问题,如排列组合问题、N皇后问题等。回溯算法实际上是一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就"回溯"返回,尝试别的路径。
算法面试题:解决n-皇后问题,即在n*n的棋盘上放置n个皇后,使得它们不能相互攻击。
js
function solveNQueens(n) {
const result = [];
const board = new Array(n).fill().map(() => new Array(n).fill('.'));
const helper = (row) => {
if (row === n) {
result.push(board.map(r => r.join('')));
return;
}
for (let col = 0; col < n; col++) {
if (isValid(row, col, n, board)) {
board[row][col] = 'Q';
helper(row + 1);
board[row][col] = '.'; // 回溯
}
}
};
helper(0);
return result;
}
哈希算法:
应用场景:用于快速查找(如哈希表)、密码学(如加密算法)、唯一标识(如数字签名)以及数据校验(如校验和)等。
算法面试题 :设计一个哈希映射,它应该包括put
、get
、delete
等操作方法。
js
class HashMap {
constructor() {
this.size = 1000;
this.map = new Array(this.size);
for (let i = 0; i < this.size; i++) {
this.map[i] = new LinkedList();
}
}
put(key, value) {
let index = this.hashCode(key);
this.map[index].insertOrUpdate({key, value});
}
get(key) {
let index = this.hashCode(key);
return this.map[index].search(key);
}
delete(key) {
let index = this.hashCode(key);
this.map[index].delete(key);
}
hashCode(key) {
// 这里可以是任何哈希函数的实现,为了避免大家说我水字数,这里使用简单的取模运算
return key % this.size;
}
}
数据结构
数据结构是计算机存储、组织数据的方式,它可以帮助我们高效地获取和修改数据。而作为一个程序员,了解和掌握必备的数据结构知识是必须的。
1. 数组(Array):
- 构建:直接声明或者使用构造函数创建数组。
- 修改:通过索引直接访问并赋值。
- 遍历:使用
forEach
,for
, 或for...of
等循环。
js
// 创建并初始化数组
const arr = [1, 2, 3, 4, 5];
// 修改数组中的元素
arr[2] = 10;
// 遍历数组
arr.forEach(item => console.log(item));
2. 堆(Heap):
- 构建:通常用数组表示,特别是二叉堆。
- 修改:实现添加和删除方法,保持堆的特性(最小堆或最大堆)。
- 遍历:由于堆是二叉树的一种,可以用树的遍历方法,但通常堆用于优先队列和堆排序,不常直接遍历。
js
// 定义一个最小堆(MinHeap)类
class MinHeap {
// 构造函数,初始化最小堆
constructor() {
// 堆用数组形式存储,初始为空数组
this.heap = [];
}
// 省略具体方法的实现(如 insert、extractMin 等)
// ... (这里应补充插入、调整堆等方法的具体实现)
}
// 创建一个MinHeap实例
const minHeap = new MinHeap();
// 向最小堆中添加元素2
minHeap.insert(2);
// 向最小堆中添加元素1
minHeap.insert(1);
// 获取并删除最小堆中的最小值
minHeap.extractMin();
3.栈(Stack):
- 构建:使用数组模拟栈。
- 修改:使用
push
添加元素到栈顶,用pop
从栈顶移除元素。 - 遍历:通常栈不需要遍历,操作都是在栈顶进行。
js
// 创建一个空栈
let stack = [];
// 压入元素
stack.push(2);
// 弹出元素
stack.pop();
4.队列(Queue):
- 构建:使用数组模拟队列。
- 修改:使用
push
添加元素到队列尾,用shift
从队列头移除元素。 - 遍历:使用循环遍历。
js
// 构建一个队列
let queue = [];
// 入队操作
queue.push(2);
// 出队操作
queue.shift();
5. 链表(LinkedList):
- 构建:定义节点类,构建链表类管理节点。
- 修改:实现添加、删除节点方法。
- 遍历:从头节点开始,使用next指针遍历。
js
// 定义一个单链表节点类
class ListNode {
// 构造函数,接收节点值作为参数
constructor(value) {
// 节点值
this.value = value;
// 指向下一个节点的指针,默认为null
this.next = null;
}
}
// 定义一个单链表类
class LinkedList {
// 构造函数,初始化链表
constructor() {
// 头节点,初始时为null
this.head = null;
}
}
// 创建一个LinkedList实例
let list = new LinkedList();
// 构建链表:创建一个值为1的新节点,并将其设置为链表的头节点
list.head = new ListNode(1);
6. 树(Tree):
- 构建:建立节点类,每个节点存有子节点的引用。
- 修改:实现添加、删除节点方法。
- 遍历:使用递归或迭代方法遍历,如前序、中序、后序、层次遍历。
js
// 定义一个二叉树节点类
class TreeNode {
// 构造函数,接收节点值作为参数
constructor(value) {
// 节点值
this.value = value;
// 左子节点指针,默认为null
this.left = null;
// 右子节点指针,默认为null
this.right = null;
}
}
// 创建一个二叉树根节点实例,值为1
const root = new TreeNode(1);
// 向根节点添加左子节点,值为2
root.left = new TreeNode(2);
// 向根节点添加右子节点,值为3
root.right = new TreeNode(3);
7. 图(Graph):
- 构建:使用邻接列表或邻接矩阵保存节点间的关系。
- 修改:实现添加节点、添加边的方法。
- 遍历:使用深度优先搜索(DFS)或广度优先搜索(BFS)遍历图中的节点。
js
// 定义一个图类
class Graph {
// 构造函数,初始化图
constructor() {
// 采用邻接列表方式存储图,键为顶点,值为与该顶点相连的其他顶点数组
this.adjacencyList = {};
}
// 添加顶点方法,接收顶点作为参数
addVertex(vertex) {
// 如果当前图中不存在该顶点,则在邻接列表中创建一个以该顶点为键、值为空数组的项
if (!this.adjacencyList[vertex]) this.adjacencyList[vertex] = [];
}
// 添加边方法,接收两个顶点作为参数,表示它们之间存在一条边
addEdge(vertex1, vertex2) {
// 将顶点2添加到顶点1对应的邻接列表项中
this.adjacencyList[vertex1].push(vertex2);
// 将顶点1添加到顶点2对应的邻接列表项中(假设图是无向的)
this.adjacencyList[vertex2].push(vertex1);
}
}
// 创建一个Graph实例
const graph = new Graph();
// 添加顶点'A'到图中
graph.addVertex('A');
// 添加顶点'B'到图中
graph.addVertex('B');
// 在图中添加一条连接顶点'A'和顶点'B'的边
graph.addEdge('A', 'B');