JavaScript 版数据结构与算法
简介
数据结构与算法:
- 数据结构:计算机存储、组织数据的方式
- 算法:一系列解决问题的清晰指令
- 程序 = 数据结构 + 算法
- 数据结构为算法提供服务,算法围绕数据结构进行操作
LeetCode 是什么:
- LeetCode 官网:https://leetcode-cn.com
- LeetCode 是一个专注于程序员技术成长和企业技术人才服务的品牌
- 功能:题库、社区、竞赛、模拟面试等等
一、时间 / 空间复杂度计算
1、时间复杂度计算
- 一个函数,用大 O 表示,比如 O(1)、O(n)、O(logN)、O(n^2) ……
- 定性描述该算法的运行时间
2、空间复杂度计算
- 一个函数,用大 O 表示,比如 O(1)、O(n)、O(n^2) ……
- 算法在运行过程中临时占用存储空间大小的度量
二、数据结构
1、栈(Stack)
栈是什么:
- 一个 先进后出 的数据结构
- JavaScript 中没有栈,但可以用 Array 实现栈的所有功能
- push 和 pop 实现 入栈 和 出栈
- 或者使用:unshift 和 shift 实现 入栈 和 出栈
栈的应用场景:
- 需要 先进后出 的场景
- 例如:十进制转二进制、判断字符串的括号是否有效、函数调用堆栈……
判断字符串的括号是否有效(leetcode:20):
- 越靠后的左括号,对应的右括号越靠前
- 左括号入栈,右括号出栈,最后栈空了就是合法的
const isValid = function( str ) {
// 如果字符串长度为奇数,直接可得不合法
if(str.length % 2 === 1) return false;
const stack = [];
for(let i = 0; i < str.length; i++) {
const a = str[i];
if(a === '(' || a === '[' || a === '{') {
stack.push(a);
} else {
const top = stack[stack.length - 1]
if((top === '(' && a === ')') || (top === '[' && a === ']') || (top === '{' && a === '}') ) {
stack.pop();
} else {
return false;
}
}
}
return stack.length === 0;
}
2、队列(Queue)
队列是什么:
- 一个 先进先出 的数据结构
- JavaScript 中没有队列,但可以用 Array 实现队列的所有功能
- push 和 shift 实现队列的 入队 和 出队
- 或者使用:unshift 和 pop 实现队列的 入队 和 出队
队列的使用场景:
- 需要 先进先出 的场景
- 例如:JS 异步中的任务队列、计算最近请求次数……
JS 异步中的任务队列:
- JS 是单线程,无法同时处理异步中的并发任务
- 使用任务队列先后处理异步任务
setTinmeout(() => {
console.log(1)
}, 0)
console.log(2)
// 输出结果为:
2
1
最近请求次数(leetcode:933):
- 有新请求就入队,3000ms 前发出的请求出对
- 队列的长度就是最近请求次数
var RecentCounter = function() {
this.queue = [];
}
RecentCounter.prototype.ping = function(t) {
this.queue.push(t);
while(this.queue[0] < t - 3000) {
this.queue.shift();
}
return this.queue.length;
}
3、链表(LinkedList)
链表是什么:
- 多个元素组成的列表
- 元素存储不连续,用 next 指针连在一起
- JavaScript 中没有链表这种数据结构,可以用 Object 模拟链表
数组和链表的区别:
- 数组:增删非首尾元素时往往需要移动元素
- 链表:增删非首尾元素,不需要移动元素,只需要更改 next 的指向即可
操作链表:
const a = { val: 'a' }
const b = { val: 'b' }
const c = { val: 'c' }
const d = { val: 'd' }
a.next = b;
b.next = c;
c.next = d;
// 遍历链表
let p = a;
while(p) {
console.log(p.val)
p = p.next;
}
// 在链表中插入新值: 在 c 和 d 之间插入 e
const e = { val: 'e' }
c.next = e;
e.next = d;
// 删除
c.next = d
删除链表中的节点(leetcode:237):
- 编写一个函数,使其可以删除某个链表中给定的非末尾节点,只会给被删除的节点
- 无法直接获取被删除节点的上一个节点
- 将被删除节点转移到下一个节点
- 思路:将被删节点的值改为下一个节点的值
- 删除下一个节点
var deleteNode = function(node) {
node.val = node.next.val;
node.next = node.next.next;
}
反转链表(leetcode:206):
- 输入:1 -> 2 -> 3 -> 4 -> 5 -> null
- 输出:5 -> 4 -> 3 -> 2 -> 1 -> null
- 解题思路:反转两个节点:将 n+1 的 next 指向 n
- 反转多个节点:双指针遍历链表,重复上述操作
var reverseLinkedList = function(head) {
let p1 = head;
let p2 = null;
while(p1) {
const tmp = p1.next;
p1.next = p2;
p2 = p1;
p1 = tmp;
}
return p2;
}
两数相加(leetcode:2):
- 解题步骤:新建一个空链表
- 遍历被相加的两个链表,模拟相加操作,将“个位数”追加到新链表上,将“十位数”留到下一位去相加
var addTwoNumbers = function(l1, l2) {
const l3 = new ListNode(0);
let p1 = l1;
let p2 = l2;
let p3 = l3;
let carry = 0;
while(p1 || p2) {
const v1 = p1 ? p1.val : 0;
const v2 = p2 ? p2.val : 0;
const val = v1 + v2;
carry = Math.floor(val / 10);
p3.next = new ListNode(val % 10);
if(p1) p1 = p1.next;
if(p2) p2 = p2.next;
p3 = p3.next;
}
if(carry) {
p3.next = new ListNode(carry);
}
return l3.next;
}
删除排序链表中的重复元素(LeetCode:83):
- 解题思路:因为链表是有序的,所以重复元素一定相邻
- 遍历链表,如果发现当前元素和下一个元素值相同,就删除下一个元素值
var deleteDuplicates = function(head) {
let p = head;
while(p && p.next) {
if(p.val === p.next.val) {
p,next = p.next.next;
} else {
p = p.next;
}
}
return head;
}
判断链表是否有环(LeetCode:141):
- 解题思路:用一快一慢两个指针遍历链表,如果指针能够相逢,那么链表就有环
var hasCycle = function(head) {
// 慢的指针
let p1 = head;
// 快的指针
let p2 = head;
while(p1 && p2 && p2.next) {
p1 = p1.next;
p2 = p2.next.next;
if(p1 === p2) {
return true;
}
}
return false;
}
4、集合(Set)
集合是什么:
- 一种 无序且唯一 的数据结构(无序不可重复)
- ES6 中有集合,名为 Set
- 集合的常用操作:去重、判断某元素是否在集合中、求交集……
// 去重
const arr = [1,1,2,3,4,4,5];
const arr2 = [...new Set(arr)];
// 判断元素是否在集合中
const set = new Set(arr);
const has = set.has(1);
console.log(has); // true
// 求交集
const set1 = new Set([2,3,6]);
const set3 = new Set([...set].filter(item => {
set2.has(item);
}));
Set 操作:
- 使用 Set 对象:new、add、delete、has、size
- 迭代 Set:多种迭代方法、Set 与 Array 互转、求交集 / 差集
// new
let mySet = new Set();
// add
mySet.add(1);
mySet.add(2);
mySet.add(2);
mySet.add('hello world');
let o = { a: 1, b: 2 };
mySet.add(o);
mySet.add({ a: 1, b: 2 });
// has
const has = mySet.has(1); // true
const has2 = mySet.has(3); // false
// delete
mySet.delete(2);
// size
const size = mySet.size;
// 迭代
for(let item of mySet) {
console.log(item)
}
// 或
for(let item of mySet.keys()) {
console.log(item)
}
// 或
for(let item of mySet.values()) {
console.log(item)
}
// Set 转为 Array
const arr = [...mySet];
const arr2 = Array.from(set);
// Array 转为 Set
const set = new Set([1,3,5]);
// 交集 intersection
const intersection = new Set([...mySet].filter(x => set.has(x)));
// 差集 difference
const difference = new Set([...mySet].filter(x => !set.has(x)))
两个数组的交集(LeetCode:349):
- 解题步骤:用集合对 nums1 去重
- 遍历 nums1,筛选出 nums2 也包含的值
var intersection = function(nums1, nums2) {
return [...new Set(nums1)].filter(n => {
// new Set(nums2).has(n);
// 或者
nums2.includes(n);
})
}
5、字典(Map)
字典是什么:
- 与集合类似,字典也是一种存储唯一值的数据结构,但它是以 键值对 的形式来存储的
- ES6 中有字典,名为 Map
- 字典的常用操作:键值对的增、删、改、查
// 实例化
const map = new Map();
// 增
map.set('a','aa');
map.set('b', 'bb');
// 查
map.get('a');
map.get('b');
// 删
map.delete('b');
// 清空
map.clear();
// 改:就是覆盖
map.set('a', 'aaaaa');
两个数组的交集(LeetCode:349):
- 解题步骤:新建一个字典,遍历 nums1 ,填充字典
- 遍历 nums2,遇到字典里的值就选出,并从字典中删除
var intersection = function(nums1, nums2) {
const map = new Map();
nums1.forEach(n => {
map.set(n, true);
});
const res = [];
nums2.forEach(n => {
if(map.get(n)) {
res.push(n);
map.delete(n);
}
});
return res;
}
判断字符串的括号是否有效(leetcode:20):
- 越靠后的左括号,对应的右括号越靠前
- 左括号入栈,右括号出栈,最后栈空了就是合法的
const isValid = function( str ) {
// 如果字符串长度为奇数,直接可得不合法
if(str.length % 2 === 1) return false;
const stack = [];
const map = new Map();
map.set('(', ')');
map.set('[', ']');
map.set('{', '}');
for(let i = 0; i < str.length; i++) {
const a = str[i];
if(map.has(a)) {
stack.push(a);
} else {
const top = stack[stack.length - 1]
if(map.get(top) === a) {
stack.pop();
} else {
return false;
}
}
}
return stack.length === 0;
}
两数之和(LeetCode:1):
var twoSum = function(nums, target) {
const map = new Map();
for(let i = 0; i < nums.length; i++) {
const n1 = nums[i];
const n2 = target - n;
if(map.has(n2)) {
return [map.get(n2), i];
} else {
map.set(n1, i);
}
}
}
无重复字符的最长字串(LeetCode:3):
- 用双指针维护一个滑动窗口,用来剪切字串
- 不断移动右指针,遇到重复字符,就把左指针移动到重复字符的下一位
- 过程中,记录所有窗口的长度,并返回最大值
var lengthOfLongSubstring = function(str) {
let p = 0;
let count = 0;
const map = new Map();
for(let i = 0; i < str.length; i += 1) {
if(map.has(str[i]) && map.get(str[i] >= p)) {
p = map.get(str[i] + 1);
}
count = Math.max(count, i - p + 1);
map.set(str[i], i);
}
return count;
}
最小覆盖字串(LeetCode:76):
- 解题步骤:用双指针维护一个滑动窗口
- 移动右指针,找到包含 t 的字串,移动左指针,尽量减少包含 t 的字串的长度
- 循环上述过程,找到包含 t 的最小字串
var minWindow = function(s, t) {
let l = 0;
let r = 0;
const map = new Map();
for(let item of t) {
map.set(item, map.has(item) ? map.get(item + 1) : 1);
}
let needType = map.size;
let res = '';
while(r < s.length) {
const c = s[r];
if(map.has(c)) {
map.set(c, map.get(c) - 1);
if(map.get(c) === 0) {
needType -= 1;
}
}
while(needType) {
const newRes = s.substring(l , r + 1);
if(!res || newRes.length <= res.length) {
res = newRes;
}
const c2 = s[l];
if(map.has(c2)) {
map.set(c2, map.get(c2) + 1);
if(map.get(c2) === 1) {
needType += 1;
}
}
l += 1;
}
r += 1;
}
return res;
}
6、树(Tree)
7、图()
8、堆()
三、进阶算法之 “搜索与排序”
1、排序和搜索简介
排序和搜索是什么:
- 排序:把某个乱序的数据结构按照某种顺序进行排序,例如:数组变成升序或降序的数组(sort() 方法)
- 搜索:通过某个关键字进行查找,例如:找出数组中某个元素的下标(indexOf() 方法)
排序算法:
- 冒泡排序
- 选择排序
- 插入排序
- 归并排序
- 快速排序
- ……
搜索算法:
- 顺序搜索
- 二分搜索
- ……
2、冒泡排序
- 冒泡排序的思路:
比较所有相邻元素,如果第一个比第二个大,则交换;一轮下来,可以保证最后一个数是最大的
执行 n - 1 轮,就可以完成排序
- 时间复杂度:使用两个嵌套循环,时间复杂度为 O(n^2)
const arr = [1, 3, 2, 8, 7, 3, 5, 1, 4]
// 冒泡排序
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
}
Array.prototype.bubbleSort = function () {
let arr = this
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 this
}
console.log(bubbleSort(arr)) //原数组已改变 [ 1, 1, 2, 3, 3, 4, 5, 7, 8]
console.log("打印原数组=>", arr) // [ 1, 1, 2, 3, 3, 4, 5, 7, 8]
arr.bubbleSort()
console.log(arr) // 原数组已改变 [ 1, 1, 2, 3, 3, 4, 5, 7, 8]
3、选择排序
4、插入排序
5、归并排序
6、快速排序
- 快速排序的思路:
分区:从数组中任意选择一个“基准”,所有比基准小的元素放在基准前面,比基准大的元素放在基准的后面
递归:使用递归的方式对基准前后的子数组进行分区
退出条件:当子数组的长度小于等于1时结束递归
- 时间复杂度:分区的时间复杂度是 O(n),递归的时间复杂度是 O(logN),所以快速排序的时间复杂度为 O(n * logN)
const arr = [1, 3, 2, 8, 7, 3, 5, 1, 4]
// 快速排序
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)
}
Array.prototype.quickSort = function () {
const sort = (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)
}
const result = sort(this)
result.forEach((item, index) => this[index] = item)
}
console.log(quickSort(arr)) // 有序,不改变原数组 [1, 1, 2, 3, 3, 4, 5, 7, 8]
console.log("打印原数组=>", arr) // [1, 3, 2, 8, 7, 3, 5, 1, 4]
arr.quickSort()
console.log(arr) // 有序,原数组已更改 [1, 1, 2, 3, 3, 4, 5, 7, 8]
7、顺序搜索
8、二分搜索
- 二分查找的思路:
从数组的中间元素开始,如果中间元素正好是目标值,则搜索结束,返回中间元素的下标
如果目标值大于或小于中间元素,则在大于或小于中间元素的那一半数组中搜索
如果目标值不在数组中,则返回 -1
- 时间复杂度:每次比较都使搜索范围缩小一半,时间复杂度为 O(logN)
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 二分查找
function binarySearch(arr, target) {
let left = 0
let right = arr.length - 1
while (left <= right) {
let mid = Math.floor((right + left) / 2)
let ele = arr[mid]
if (ele < target) {
left = mid + 1
} else if (ele > target) {
right = mid - 1
} else {
return mid
}
}
return -1
}
Array.prototype.binarySearch = function (target) {
let arr = this
let left = 0
let right = arr.length - 1
while (left <= right) {
let mid = Math.floor((right + left) / 2)
let ele = arr[mid]
if (ele < target) {
left = mid + 1
} else if (ele > target) {
right = mid - 1
} else {
return mid
}
}
return -1
}
console.log(binarySearch(arr, 1)) // 0
console.log(arr.binarySearch(10)) // -1