数据结构
常见的数据结构有:栈、队列、链表、集合、字典、树、图、堆等等
栈
-
栈是一种后进先出的数据结构,可以通过数组Array的push和pop方法来模拟栈这种数据结构。
-
栈的使用场景:十进制转二进制、判断字符串的括号是否有效、函数调用栈、二叉树的前序遍历等等
class Stack {
constructor() {
this.list = [];
}
push(item) {
return this.list.push(item);
}
pop() {
return this.list.pop();
}
peek() {
return this.list[this.list.length - 1];
}
}
队列
-
队列是一种先进先出的数据结构,可以通过数组Array的push和shift方法来模拟栈这种数据结构。
-
队列的使用场景:食堂打饭、火车站买票、异步任务队列、最近的请求次数等等
class Queue {
constructor() {
this.list = [];
}
push(item) {
return this.list.push(item);
}
shift() {
return this.list.shift();
}
peek() {
return this.list[0];
}
}
链表
-
链表是多个元素组成的列表。
-
和数组不同的是,链表的存储不是连续的,用next指针来连接起来。
js中的链表使用Object来实现。 -
链表的基本操作:遍历链表、插入链表、删除链表
class ListNode {
constructor(val, next) {
this.val = val;
this.next = next;
}
}
集合
- 集合是无序且唯一的;ES中使用Set来表示集合;
- 集合的Set的使用:new, has,add,delete,size
const set = new Set() // Set(0) {size: 0}
set.add('a') // Set(1) {1}
set.size // 1
set.has('a') // true
set.delete('a')
字典
-
和集合类似,字典也是存储唯一值的数据结构,不同的是字典是以键值对的形式来存储;表示一种映射关系。
-
ES6中使用Map来表示字典。
const map = new Map() // Map(0) {size: 0}
map.set('a', 1) // Map(1) {'a' => 1}
map.size // 1
map.has('a') // true
map.set('a', 100) // Map(1) {'a' => 100}
map.get('a') // 100
map.delete('a') // true
map.clear() // undefined
树
-
树是一种分层数据的抽象模型
-
前端中常见的树有:DOM树,级联菜单,树形控件等等
-
JS中没有树,前端可以用Object和Array来构建树。
-
树的常用操作:深度优先遍历和广度优先遍历
-
二叉树的常用操作:前序遍历、中序遍历、后序遍历
{
value: 'a',
lable: '第一个',
childen: [
{
value: 'b',
lable: '第二个',
},
{
value: 'c',
lable: '第三个',
},
]
}
图
-
图是网络结构的抽象模型,是一组由边连接的节点
-
图可以表示任何二元关系,比如道路,航班
-
js中用Object 和 Array来表示图
-
图的表示法: 邻接矩阵、邻接表、关联矩阵…
-
图的常用操作:深度优先遍历和广度优先遍历
堆
-
完全二叉树是指每层节点全部填满,最后一层如果不是满的,则只缺少右边的若干节点。
-
堆是一种特殊的完全二叉树
-
所有节点都大于等于(最大堆)或者小于等于(最小堆)它的子节点
-
堆能快速高效的找出最大值,最小值;时间复杂度为O(1)
排序和搜索
常见的排序算法有:冒泡排序、选择排序、插入排序、归并排序、快速排序
常见的搜索算法有:顺序搜索、二分搜索
冒泡排序
- 比较相邻元素,如果第一个比第二个大,则交换他们
- 一轮下来保证最后一个是最大的
- 执行n-1轮,完成排序
Array.prototype.bubbleSort = function () {
for (let i = 0; i < this.length - 1; i++) {
for (let j = 0; j < this.length - 1 - i; j++) {
if (this[j] > this[j + 1]) {
const temp = this[j];
this[j] = this[j + 1];
this[j + 1] = temp;
}
}
}
}
时间复杂度:O(n^2)
空间复杂度:O(1)
选择排序
- 选择数组中的最小值,并将其放到第一位
- 接着寻找第二小的值,放到第二位
- 依次执行n-1轮,选择排序完成
Array.prototype.selectSort = function () {
for (let i = 0; i < this.length - 1; i++) {
let minIndex = i;
for (let j = i; j < this.length; j++) {
if (this[j] < this[minIndex]) {
minIndex = j
}
}
if (i !== minIndex) {
const temp = this[i];
this[i] = this[minIndex];
this[minIndex] = temp;
}
}
}
arr.selectSort()
时间复杂度:O(n^2)
空间复杂度:O(1)
插入排序
- 从第二个数开始往前比
- 如果有比它大的数就往后移
- 依次类推,进行到最后一个数
Array.prototype.insertSort = function () {
for (let i = 1; i < this.length; i++) {
const temp = this[i];
let j = i;
while (j > 0) {
if (this[j] < this[j - 1]) {
this[j] = this[j - 1];
j--;
} else {
break
}
}
this[j] = temp;
}
}
const arr1 = [6, 8, 5, 9, 3, 2, 1]
arr1.selectSort()
console.log(arr1)
时间复杂度:O(n^2)
空间复杂度:O(1)
归并排序
- 分:把数组劈成两半,再递归对子数组进行“分”的操作,直至分成一个个单独的数
- 合:把两个数合并为有序数组,再对有序数组进行合并,直至全部子数组合并为一个完整数组
(1)新建一个空数组res,用于存放最终的数组
(2)比较两个有序数组的头部,较小者出队并推入res中
(3)如果两个数组还有值,就重复第二步
Array.prototype.mergeSort = function () {
const rec = (arr) => {
if (arr.length === 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = arr.slice(0, mid);
const right = arr.slice(mid)
const orderLeft = rec(left)
const orderRight = rec(right)
const res = []
while (orderLeft.length || orderRight.length) {
if (orderLeft.length && orderRight.length) {
res.push(orderLeft[0] < orderRight[0] ? orderLeft.shift() : orderRight.shift())
} else if (orderLeft.length) {
res.push(orderLeft.shift())
} else if (orderRight.length) {
res.push(orderRight.shift())
}
}
return res;
}
rec(this)
}
const arr2 = [6, 8, 5, 9, 3, 2, 1]
const res = arr2.mergeSort()
console.log(res)
时间复杂度:O(nlogn)
空间复杂度:O(n)
快速排序
- 分区:以数组中某个元素为基准,找出所有比他小的放前边,找出所有比他大的放后面;
- 递归:递归对基准前后的子数组进行分区
- 递归结束后返回排序后的数组
Array.prototype.quickSort = function () {
const rec = (arr) => {
if (arr.length <= 1) {
return arr
}
const left = [];
const right = [];
const mid = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] < mid) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return [...rec(left), mid, ...rec(right)];
}
const res = rec(this)
res.forEach((n, i) => {
this[i] = n
})
}
const arr = [6, 8, 5, 9, 3, 2, 1];
arr.quickSort();
console.log(arr);
时间复杂度:O(nlogn)
空间复杂度:O(n)
顺序搜索
-
数组从头开始遍历,找出相应的元素下标,找不到返回-1
-
时间复杂度为O(n),搜索效率低
function sequential(arr, target) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) {
return i;
}
}
return -1;
}
sequential([1, 2, 3, 4], 3)
二分搜索
二分搜索只适用于有序数组;如果是乱序数组,需要先排序再进行二分搜索
- 从数组的中间元素开始,如果中间元素等于目标元素,搜索结束
- 如果目标值大于或者小于中间元素,则在大于或者小于中间元素的那一半数组中搜索
function binarySearch(arr, target) {
let start = 0;
let end = arr.length - 1;
while (start <= end) {
let mid = Math.floor((start + end) / 2);
if (target > arr[mid]) {
start = mid + 1;
} else if (target < arr[mid]) {
end = mid - 1;
} else {
return mid
}
}
return -1;
}
binarySearch([1, 2, 3, 4], 5)
时间复杂度:O(logn)
空间复杂度:O(1)
算法思想
常见的算法思想有:分而治之、动态规划、贪心算法、回溯算法等等
分而治之
-
它将一个问题分为多个和原问题相似的小问题,递归或迭代解决小问题,再将结果合并来解决原来的问题。
-
使用场景:归并排序、快速排序、猜数字大小、翻转二叉树、对称二叉树等等
动态规划
-
动态规划将一个问题分解成
互相重叠
的子问题,通过反复解决子问题来解决原来的问题。 -
动态规划是分解成
互相重叠
的子问题;分而治之是分解为相互独立
的子问题 -
使用场景:爬楼梯、打家劫舍等等
贪心算法
-
期望通过每个阶段的局部最优选择,达到全局最优
-
但是结果不一定最优
-
使用场景:分饼干、买卖股票等等
回溯算法
-
回溯算法是一种渐进式寻找并构建问题解决方式的策略
-
回溯算法从一个可能的动作开始,如果不行,就回溯选择另外一个,直到将问题解决。
-
使用场景:数组全排列、找数组子集等等