本文总结基础的数据结构算法题的JS实现:
看需求:👇
手撕前端基础题,请左拐:
leetcode高频算法题的JS/Python实现,请右拐:
博主正努力学习,抓紧更新中,冲鸭~
数据结构与算法JS实现
前言
别问,问就是一起卷。(还嫌不够卷吗,/(ㄒoㄒ)/~~)
一、排序算法
1.1 冒泡
// 冒泡排序
const bubbleSort = (arr)=>{
if (arr.length <=1) return
for(let i=0;i<arr.length;i++) {
let hasChange = false;
for (let j=0;j<arr.length-i-1;j++) {
if (arr[j] > arr[j+1]) {
const temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
hasChange = true
}
}
// 当hasChange为false时,所有元素已到位
if(!hasChange) break
}
console.log(arr);
};
1.2 插入
const insertionSort = (arr) => {
if (arr.length <= 1) return
for (let i = 1; i < arr.length; i++) {
const temp = arr[i]
let j = i - 1
// 若arr[i]前有大于arr[i]的值的化,向后移位,腾出空间,直到一个<=arr[i]的值
for (j; j >= 0; j--) {
if (arr[j] > temp) {
arr[j + 1] = arr[j]
} else {
break
}
}
arr[j + 1] = temp
}
console.log(arr)
}
1.3 选择
// 选择排序
const selectionSort = (arr) => {
if (arr.length <= 1) return
// 需要注意这里的边界, 因为需要在内层进行 i+1后的循环,所以外层需要 数组长度-1
for (let i = 0; i < arr.length - 1; i++) {
let minIndex = i
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j // 找到整个数组的最小值
}
}
const temp = arr[i]
arr[i] = arr[minIndex]
arr[minIndex] = temp
}
console.log(arr)
}
const test = [4, 5, 6, 3, 2, 1]
bubbleSort(test)
const testSort = [4, 1, 6, 3, 2, 1]
insertionSort(testSort)
const testSelect = [4, 8, 6, 3, 2, 1, 0, 12]
selectionSort(testSelect)
1.4 快排(⭐)
// 选择一个元素作为"基准"
// 小于"基准"的元素,都移到"基准"的左边;大于"基准"的元素,都移到"基准"的右边。
// 对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。
function quickSort(arr) {
// 交换元素
function swap(arr, a, b) {
var temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
// 分区函数
const partition = (arr,pivot,left,right)=>{
const pivotVal = arr[pivot];
let startIndex = left;
for(let i=left;i<right;i++) {
if(arr[i]<pivotVal) {
swap(arr,i,startIndex);
startIndex++
}
}
swap(arr,startIndex,pivot);
return startIndex
};
//快排主函数
const sort = (arr,left,right)=>{
if(left < right) {
let pivot = right;
let partitionIndex = partition(arr,pivot,left,right);
sort(arr,left,partitionIndex-1<left?left:partitionIndex-1);
sort(arr,partitionIndex+1>right?right:partitionIndex+1,right)
}
};
sort(arr,0,arr.length-1);
return arr;
}
console.log(quickSort([6,5,7,2,4,2,4,2,6,1,1])); // [1,1,2,2,2,4,4,5,6,6]
1.5 归并
/**
* @Description: 归并排序的核心思想是分治,把一个复杂问题拆分成若干个子问题来求解。
* 归并排序的算法思想是:把数组从中间划分为两个子数组,
* 一直递归地把子数组划分成更小的数组,直到子数组里面只有一个元素的时候开始排序。
* 排序的方法就是按照大小顺序合并两个元素。
* 接着依次按照递归的顺序返回,不断合并排好序的数组,直到把整个数组排好序。
* @author Li,Weixin
* @date 2021/9/7
*/
const mergeArr = (left, right) => {
let temp = [];
let leftIndex = 0;
let rightIndex = 0;
// 判断2个数组中元素的大小,一次插入数组
while (left.length > leftIndex && right.length > rightIndex) {
if (left[leftIndex] <= right[rightIndex]) {
temp.push(left[leftIndex]);
leftIndex++
} else {
temp.push(right[rightIndex]);
rightIndex++
}
}
return temp.concat(left.slice(leftIndex)).concat(right.splice(rightIndex))
};
const 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 mergeArr(mergeSort(left), mergeSort(right))
};
const testArr = [];
let i = 0;
while (i < 100) {
testArr.push(Math.floor(Math.random() * 1000));
i++
}
const res = mergeSort(testArr);
console.log(res)
1.6 基数
二、二分查找(⭐)
Binary Search :一种非常高效的查找算法,时间复杂度O(logn),解释如下:
// 二分查找得非递归实现
function binary_search(nums,target) {
let left = 0;
let right = nums.length - 1;
while (left <= right) { //循环退出条件
mid = left + ((right - left) >> 1); //位运算求中位数
if (target === nums[mid]) {
return mid
} else if (target < nums[mid]) {
right = mid - 1
} else {
left = mid + 1
}
}
return -1
}
//二分查找得递归实现
function binary_research(arr,left,right,key) {
if (left > right) {
return -1
}
let mid = left + ((right-left)>>1);
if(arr[mid] === key){
return mid;
}else if (arr[mid] > key){
return binary_research(arr,left,mid-1,key)
}else{
return binary_research(arr,mid+1,right,key)
}
}
二分查找的局限性:
- 数组必须有序
- 数据量太小,顺序遍历即可;数据量太大也不好,二分查找底层依赖数组这周数据结构,需要连续的内存空间。
2.1 求解平方根
三、链表相关
3.1 反转单链表
//非递归实现
var reverseList = function(head) {
//let [prev, curr] = [null, head];
let prev=null;
let curr=head;
while (curr) {
let tmp = curr.next; // 1. 临时存储当前指针后续内容
curr.next = prev; // 2. 反转链表
prev = curr; // 3. 接收反转结果
curr = tmp; // 4. 接回临时存储的后续内容
}
return prev;
};
3.2 未排序链表去重
//移除未排序链表中的重复节点。保留最开始出现的节点。(两层循环)
//输入:[1, 2, 3, 3, 2, 1]
//输出:[1, 2, 3]
const removeDuplicateNodes = (head) => {
let p = head;
while (p) {
let q = p;
while (q.next) {
if (q.next.val === p.val) {
q.next = q.next.next;
} else {
q = q.next;
}
}
p = p.next;
}
return head
}
3.3 排序列表去重
const deleteDuplicates = (head) => {
let current = head; //把首节点指针赋给current
while (current && current.next) { //当前节点及下一节点不为空
if (current.val === current.next.val) {
current.next = current.next.next;
}else{
current = current.next
}
}
return head //返回首节点
}
3.4 单链表删除节点
//给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
//返回删除后的链表的头节点。
const deleteNode = (head, val) => {
let pre = new ListNode(0); // 额外添加头节点 哨兵节点 考虑要删除的节点在第一个
pre.next = head;
let node = pre;
while (node.next) {
if (node.next.val === val) {
node.next = node.next.next;
break;
}
node = node.next;
}
return pre.next;
}
3.5 链表partition
// 思路:一拆为二然后合并
function partition(head, x) {
let dummy1 = new ListNode(-1); //辅助节点
let dummy2 = new ListNode(-1);
let p1 = dummy1;
let p2 = dummy2;
let p = head;
while (p != null) {
if (p.val < x) {
p1 = p1.next;
} else {
p2.next = p;
p2 = p2.next;
}
p = p.next;
}
if (dummy1.next == null) {
return head;
} else {
p1.next = dummy2.next;
p2.next = null; //以null结尾
return dummy1.next;
}
}
3.6 寻找链表倒数第K个节点
3.7 删除列表倒数第N个节点
3.8 判断是否为回文链表
const isPalindrome = (head) => {
let nums = [];
while (head) {
nums.push(head.val);
head = head.next;
}
while (nums.length > 1) { //注意大于1,考虑基数个
if (nums.pop() !== nums.shift()) return false;
}
return true;
}
3.9 判断链表是否有环
const hasCycle = (head) => {
if (!head || !head.next) return false;
let slow = head;
let fast = head.next;
while (slow !== fast) {
if(!fast || !fast.next) return false;
fast = fast.next.next;
slow = slow.next;
}
return true
}
3.10 环形链表第一个入环节点
3.11 两个链表的第一个公共节点
3.12 合并两个排序列表(K个?)
3.13 奇偶链表
四、二叉树相关
4.1 前序/中序/后序遍历
- 先序遍历
var preorderTraversal = function(root) {
const res = []
function traversal (root) {
if (root !== null) {
res.push(root.val) // 访问根节点的值
traversal(root.left) // 递归遍历左子树
traversal(root.right) // 递归遍历右子树
}
}
traversal(root)
return res
}
- 中序遍历
var inorderTraversal = function(root) {
const res = []
function traversal (root) {
if (root !== null) {
traversal(root.left)
res.push(root.val)
traversal(root.right)
}
}
traversal(root)
return res
}
- 后序遍历
var postorderTraversal = function(root) {
const res = []
function traversal (root) {
if (root !== null) {
traversal(root.left)
traversal(root.right)
res.push(root.val)
}
}
traversal(root)
return res
}
4.2 反转二叉树
var invertTree = function(root) {
function traversal (root) {
if (root === null) {
return null
} else {
[root.left, root.right] = [traversal(root.right), traversal(root.left)]
return root
}
}
return traversal(root)
}
4.3 DFS/BFS
var levelOrder = function(root) {
const res = []
function traversal (root, depth) {
if (root !== null) {
if (!res[depth]) {
res[depth] = []
}
traversal(root.left, depth + 1)
res[depth].push(root.val)
traversal(root.right, depth + 1)
}
}
traversal(root, 0)
return res
}
4.4 二叉树深度
function TreeDepth(pRoot) {
//树的深度=左子树的深度和右子树深度中最大者+1
if (pRoot === null) return 0;
var leftDep = TreeDepth(pRoot.left);
var rightDep = TreeDepth(pRoot.right);
return Math.max(leftDep, rightDep) + 1;
}
4.5 二叉树的最小深度
// 最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
var minDepth = function (root) {
if (root == null) {
return 0;
}
if (root.left == null && root.right == null) {
return 1;
}
let ans = Infinity;
if (root.left != null) {
ans = Math.min(minDepth(root.left), ans);
}
if (root.right != null) {
ans = Math.min(minDepth(root.right), ans);
}
return ans + 1;
};
- 最大深度
var maxDepth = function (root) {
let res = 0
function traversal (root, depth) {
if (root !== null) {
if (depth > res) {
res = depth
}
if (root.left) {
traversal(root.left, depth + 1)
}
if (root.right) {
traversal(root.right, depth + 1)
}
}
}
traversal(root, 1)
return res
}
4.6 判断二叉树是否为平衡二叉树
// 判断是否是平衡二叉树
function IsBalanced(pRoot) { //方法1:节点重复遍历了,影响效率了
if (pRoot == null) return true;
let leftLen = TreeDepth(pRoot.left);
let rightLen = TreeDepth(pRoot.right);
return Math.abs(rightLen - leftLen) <= 1 && IsBalanced(pRoot.left) && IsBalanced(pRoot.right);
//左右子树均平衡,且左右子树高度差不超过1
}
function TreeDepth(pRoot) {
if (pRoot == null) return 0;
let leftLen = TreeDepth(pRoot.left);
let rightLen = TreeDepth(pRoot.right);
return Math.max(leftLen, rightLen) + 1;
}
// 方法2:
//改进办法就是在求高度的同时判断是否平衡,如果不平衡就返回-1,否则返回树的高度。
//并且当左子树高度为-1时,就没必要去求右子树的高度了,可以直接一路返回到最上层了
function IsBalanced(pRoot) {
return TreeDepth(pRoot) !== -1;
}
function TreeDepth(pRoot) {
if (pRoot === null) return 0;
const leftLen = TreeDepth(pRoot.left);
if (leftLen === -1) return -1;//当左子树高度为-1时,就没必要去求rightLen,可以直接一路返回到最上层了
const rightLen = TreeDepth(pRoot.right);
if (rightLen === -1) return -1;
return Math.abs(leftLen - rightLen) > 1 ? -1 : Math.max(leftLen, rightLen) + 1;
}
4.7 树找两(叶子)节点最长距离(相隔最长路径)
4.8 二叉树右视图(左)
4.9 二叉树路径总和
// 二叉树的路径总和
// 给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。存在返回true 否则false
var hasPathSum = function (root, sum) {
// 根节点为空
if (root === null) return false;
// 叶节点 同时 sum 参数等于叶节点值
if (root.left === null && root.right === null) return root.val === sum;
// 总和减去当前值,并递归
sum = sum - root.val
return hasPathSum(root.left, sum) || hasPathSum(root.right, sum);
};
4.10 二叉树的所有路径
const binaryTreePaths = (root)=>{
//设置result来存储结果
const result = [];
const recursion = (root,path=[])=> {
//设置终止条件,当前节点为空就不再往下走
if (!root) return;
//将对应的值放入path中
path.push(root.val);
//遍历左右子树
recursion(root.left, path);
recursion(root.right, path);
// 如果当前节点的左右子树均为空,则到底了
if (!root.left && !root.right) {
result.push(path.join('->'))
}
// 回退这一步的操作,我们已经走完这条路了
path.pop();
};
recursion(root,[]);
return result
};
const root={
val:1,
left: {
val:2,
left:null,
right: {val:5,left:null,right:null},
},
right: {val:3,left:null,right:null}
};
console.log(binaryTreePaths(root));
五、动态规划
5.1 斐波那契数列
// 递归法
function Fibonacci(n) {
if(n<2){
return n;
}else{
return Fibonacci(n - 1) + Fibonacci(n-2);
}
}
function Fibonacci(n, a, b) {//尾递归 不爆栈
if (n <= 1) {
return a;
}
return Fibonacci(n - 1, b, a + b)
}
5.2 最长公共子序列LCS
5.3 最长上升子序列
5.4 连续子数组(字串)的最大和
5.5 硬币找零问题
var coinChange = function (coins, amount) {
let dp = new Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (let i = 1; i <= amount; i++) {
for (let coin of coins) {
if (i - coin >= 0) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}
5.6 0-1背包问题
// * @param {*} capacity 允许负重大小
// * @param {*} wt 重量数组
// * @param {*} val 价值数组
// * @param {*} n 几件物品 val数组的长度
function knapSack (capacity, wt, val, n) {
var i, w, a, b, dp = [];
for (let i = 0; i <= n; i++) {
dp[i] = [];
}
for (let j = 0; j <= n; j++) { //dp[i][w]表示:对于前i个物品,当前背包容量为w时,这种情况下可以装下的最大价值
for (let w = 0; w <= capacity; w++) { //从0开始计数
if (j == 0 || w == 0) {
dp[j][w] = 0;
} else if (wt[j-1] <= w) {
a = val[j - 1] + dp[j - 1][w - wt[j - 1]]; // 第j个放 wt[j - 1]表示第j个物品的重量
b = dp[j - 1][w]; // 第j个不放
dp[j][w] = (a > b) ? a : b; // max(a,b)
} else {
dp[j][w] = dp[j-1][w];
}
}
}
return dp[n][capacity];
}
5.7 爬楼梯问题
六、字符串
6.1 贪心:具有给定数值的最小字符串
6.2 判断括号字符串是够有效
6.3 字符串逆序(★★★)
//JavaScript 中可以使用 split 结合 数组自带的 reverse,先转成数组,再将数组拼接成字符串。
var str = 'abc';
var reverseStr = str.split('').reverse().join('');
//经过此操作 str 还是不变
//方法二:不使用API
var x = 701120;
var digit;
var ret = 0;
var count = [];
while (x > 0) {
digit = x % 10;
count.push(digit);
ret = ret*10+digit;
x = parseInt(x / 10);
}
console.log(count.join(""));//'021107' 字符串类型
console.log(ret);//21107 Number 类型
七、全排列1与2(Leetcode)
八、数组/队列/堆栈 常见题
8.1 合并二维有序数组成一维有序数组
function merge(left, right) {
let result = []
while (left.length > 0 && right.length > 0) {
if (left[0] < right[0]) {
/*shift()方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。*/
result.push(left.shift())
} else {
result.push(right.shift())
}
}
return result.concat(left).concat(right)
}
function mergeSort(arr) {
if (arr.length === 1) {
return arr
}
while(arr.length > 1){
let arrayItem1 = arr.shift();
let arrayItem2 = arr.shift();
let mergeArr = merge(arrayItem1, arrayItem2);
arr.push(mergeArr);
}
return arr[0]
}
let arr1 = [[1,2,3],[4,5,6],[7,8,9],[1,2,3],[4,5,6]];
let arr2 = [[1,4,6],[7,8,10],[2,6,9],[3,7,13],[1,5,12]];
console.log(mergeSort(arr1))
console.log(mergeSort(arr2))
8.2 返回数组中第K个最大元素
8.3 返回数组中和为sum的个数
8.4 实现一个字典树
8.5 数组乱序(★★★)
// 方法1:arr.sort((a,b) => Math.random() - 0.5);
arr.sort(() => Math.random() - 0.5);// 使用sort
// 方法2:时间复杂度 O(n^2)
function randomSortArray(arr) {
let backArr = [];
while (arr.length) {
let index = parseInt(Math.random() * arr.length);
backArr.push(arr[index]);
arr.splice(index, 1);
}
return backArr;
}
// 方法3:时间复杂度 O(n)
function randomSortArray2(arr) {
let lenNum = arr.length - 1;
let tempData;
for (let i = 0; i < lenNum; i++) {
let index = parseInt(Math.random() * (lenNum + 1 - i));
tempData = a[index];
a[index] = a[lenNum - i]// 随机选一个放在最后
a[lenNum - i] = tempData;
}
return arr;
}
8.6 字符串逆序
// 经此操作 str还是不变
// 方法二:不使用API
let x = 501120;
let digit;
let ret = 0;
let count = [];
while (x > 0) {
digit = x%10;
count.push(digit);
ret = ret*10+digit;
x = parseInt(x/10);
}
console.log(count.join('')); //字符串类型
console.log(typeof ret) //Number类型
字符串中出现最多的字母的次数
var str = "abcaaaaacdef";
//console.log(str.length);
var obj = {};
for(let i = 0; i < str.length; i++){
if( !obj[str.charAt(i)]){
obj[str.charAt(i)] = 1;
}else{
obj[str.charAt(i)]++;
}
}
//存出现次数最多的值和次数
var number = '';
var num = 0;
console.log("obj为:",obj); //Object {a: 6, b: 1, c: 2, d: 1, e: 1…}
//i代表每一项
for(var i in obj){
//console.log("i为:",i);
//obj[i] 为每一项的值
// console.log("obj",obj[i]);
if(obj[i]>num){
num = obj[i];
number = i;
}
}
console.log("最终出现最多的字母是:"+ number + ",次数为:"+ num);//最终出现最多的字母是:a,次数为:6
---------------------------------------------------------------
var str = 'qwertyuilo.,mnbvcsarrrrrrrrtyuiop;l,mhgfdqrtyuio;.cvxsrtyiuo';
var json = {};
//遍历str拆解其中的每一个字符将其某个字符的值及出现的个数拿出来作为json的kv
for (var i = 0; i < str.length; i++) {
//判断json中是否有当前str的值
if (!json[str.charAt(i)]) {
//如果不存在 就将当前值添加到json中去
json[str.charAt(i)] = 1;
} else {
//else的话就让数组中已有的当前值的index值++;
json[str.charAt(i)]++;
}
}
//存储出现次数最多的值和次数
var number = '';
var num=0;
//遍历json 使用打擂算法统计需要的值
for (var i in json) {
//如果当前项大于下一项
if (json[i]>num) {
//就让当前值更改为出现最多次数的值
num = json[i];
number = i;
}
}
//最终打印出现最多的值以及出现的次数
console.log('出现最多的值是'+number+'出现次数为'+num)
两个棧实现队列
//用两个栈来实现一个队列,完成队列的Push和Pop操作。
var stackPush = [];
var stackPop = [];
function push(node) {
// write code here
stackPush.push(node);
}
function pop() {
if (stackPop.length===0 && stackPush.length===0) {
console.log("Queue is empty!");
} else if (stackPop.length===0) {
while (!stackPush.length===0) {
stackPop.push(stackPush.pop());
}
}
return stackPop.pop();
}
合并有序数组
var merge = function(nums1, m, nums2, n) {
// 先将nums2合并至nums1中
for(let i = 0; i<n; i++) {
nums1[m] = nums2[i];
m++;
}
// 再对nums1进行合并
nums1.sort((a, b)=>a-b);
return nums1;
};
缺点:没有利用到两个数组各自有序的前提
时间复杂度:O(m+n)log((m+n))
空间复杂度:O(log(m+n))
- 双指针法:设有两个指针,分别指向两个数组的头部,由于最后需要返回的是nums1,故需要另设一个数组nums存放nums1初始的值。
两个指针分别指向nums和nums2的头部。
指针指向值小的那个先进入数组nums1,同时对应指针后移一位
当其中一个数组已经遍历完成后,另一个数组剩下的值直接添加进数组nums1中
在复制nums1的副本时,需要对nums1进行深复制,浅复制可能会导致数值在中途发生改变。
var merge = function(nums1, m, nums2, n) {
let nums = [m]; // 将nums1的值复制一份保存在nums中
for(let i = 0; i<m; i++) {
nums[i] = nums1[i];
}
let indicator1 = 0; // 指针指向nums1的头部
let indicator2 = 0; // 指针指向nums2的头部
let index = 0;
while(index<(m+n)) {
if(indicator1<m && indicator2<n) {
// 当nums和nums2两个数组都没遍历完时,数字较小的先进入数组nums1
nums1[index++] = (nums[indicator1]<nums2[indicator2]) ? nums[indicator1++] : nums2[indicator2++];
} else {
// 否则其中一个数组已经遍历完成而另一个数组未遍历完成,则让未遍历完的数组中的值进入数组nums1
nums1[index++] = (indicator1<m) ? nums[indicator1++] : nums2[indicator2++];
}
}
return nums1;
};
- 双指针、从后到前
设有两个指针,分别指向nums1和nums2的尾部;
指针指向值大的那个先进入数组nums1,同时对应指针前移一位;
当nums2数组已经遍历完成,无论nums1的指针是否遍历结束,此时nums1已有序排列,赋值即可结束。
var merge = function(nums1, m, nums2, n) {
let indicator1 = m-1; // 指针指向nums1的末尾
let indicator2 = n-1; // 指针指向nums2的末尾
let len = m+n-1;
while(indicator2 >= 0) {
// 数字较大的一个先入数组
nums1[len--] = (nums1[indicator1]>nums2[indicator2]) ? nums1[indicator1--] : nums2[indicator2--];
}
return nums1;
};
两数之和
// 用 hashMap 存储遍历过的元素和对应的索引。
// 每遍历一个元素,看看 hashMap 中是否存在满足要求的目标数字。
// 所有事情在一次遍历中完成(用了空间换取时间)。
const twoSum = (nums, target) => {
const prevNums = {}; // 存储出现过的数字,和对应的索引
for (let i = 0; i < nums.length; i++) { // 遍历元素
const curNum = nums[i]; // 当前元素
const targetNum = target - curNum; // 满足要求的目标元素
const targetNumIndex = prevNums[targetNum]; // 在prevNums中获取目标元素的索引
if (targetNumIndex !== undefined) { // 如果存在,直接返回 [目标元素的索引,当前索引]
return [targetNumIndex, i];
} else { // 如果不存在,说明之前没出现过目标元素
prevNums[curNum] = i; // 存入当前的元素和对应的索引
}
}
}
-----------------------------
//暴力枚举法
const twoSum = function(nums, target) {
for(let i=0;i<nums.length;i++){
for(let j=i+1;j<nums.length;j++){
if(nums[i] + nums[j] == target){
return [i, j];
}
}
}
};