1️⃣排序算法
一、冒泡排序
- 比较所有相邻的元素,如果第一个比第二个大,则交换他们的位置
- 一轮比较可以保证最后一个数是最大的,然后继续遍历剩余的数
- 代码实现
const arr = [5,4,3,7,0,2,1]
let temp = 0;
//执行arr.length-1次数组中冒泡的过程
for(let i = 0; i < arr.length -1; i++){
//遍历整个数组,将数组中最小的一个移动到数组最后面
//因为每一轮循环后,都是将数组中最小的移动到后面,所以arr.length-i,数组末尾已经是最小元素就不再遍历了
for(let j = 0; j < arr.length - i; j++){
//将如果相邻元素中,第一个比第二个大,则第一个元素向后移
if(arr[j]>arr[j+1]){
temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp
}
}
}
二、选择排序
- 找到未排序的数组中的最小值,将他和第一位互换位置
- 找到未排序数组中第二小的值,选中并将其放置在第二位
- 执行n-1轮后得到的就是一个升序数组
- 代码实现
const arr = [5,4,3,7,0,2,1]
let temp = 0;
//执行arr.length-1次选择
for(let i = 0; i < arr.length - 1; i++){
//遍历整个数组
//一轮循环数组中最小的元素保证在第一位。后续循环以此类推
//所以每一轮循环结束,数组前面都是交换出来的最小值
//使j=i,可以在后续遍历中忽略前面已经确认的较小值
for( let j = i; j < arr.length - 1; j++){
//如果数组中存在元素小于第一位,则两者互换位置-
if(arr[j] > arr[j+1]){
temp = arr[j+1];
arr[j+1] = arr[i];
arr[i] = temp
}
}
}
三、插入排序
- 从第二个数开始往前比
- 遇到大于自己的就将其(大值)位置向后移一位,小于自己的就插入到这个值后面
- 以此类推执行n-1次
const arr = [0,5,2,6,1,9]
//因为是从第二位开始比所以i=1开始
for(let i = 1; i < arr.length; i++){
let temp = arr[i];
let j = i;
while( j > 0){
if( arr[j-1] > temp ){
//将大于当前元素的值向后移
arr[j] = arr[j-1]
}
//当前元素大于前面元素时跳出循环,比较下一位
//这里break跳出的是while循环,因为前面的元素比较过了,都是有序的数组,所以当arr[j-1] < temp是可以判断temp比之前所有的元素都大,跳出本次循环即可
else break;
// j-=1如果放在if前面,当前元素大于前面值时arr[j] = temp依然会将当前元素插入到前面的位置
//而放在后面break跳出循环j-=1没有执行,相当于arr[j] = arr[j],插入操作值未改变
j -= 1;
}
//交换元素位置(此时j已经执行了-1操作),如果是通过break跳出来的,实际上j并未发生改变,arr[j] = temp相当于给自身赋值
arr[j] = temp;
}
//console.log(arr)
四、归并排序
- 分:把数组从中间分成两半,在不断递归的对数组进行“分”的操作,直到分成一个个只存在单独数的数组
- 合:把不能再分的两个数组合并为有序数组,然后再对有序数组进行合并,直到全部子数组合并为一个完整的数组
合并方法:
- 新建一个空数组res用于存放最终排序后的数组
- 比较两个有序数组的头部·,较小者出队(将拆分后的数组视为为队列)并推入res数组中
- 如果数组还有值就重复一二步操作
代码实现
const merge = (arr) => {
// 如果数组长度为1,相当于不能再拆分时返回这些单个数组
if(arr.length === 1) {return arr;}
// 获取数组的中间值
const mid = Math.floor(arr.length / 2)
// 将数组从中间拆分成开,记为左右新两个数组
const left = arr.slice(0,mid)
const right = arr.slice(mid,arr.length)
// 递归拆分
const orderLeft = merge(left)
const orderRight = merge(right)
let res = []
//当拆分后的数组不为空时
while(orderLeft.length || orderRight.length){
// 如果拆分后左右都存在就比较头元素的大小
if(orderLeft.length && orderRight.length){
// 将头元素较小的取出然后加到res数组中
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;
}
const arr = [1,4,3,9,2]
console.log(merge(arr))
五、快速排序
- 分区: 从数组中任意选择一个基准,比基准大的元素放在基准后面,比基准小的元素放在基准前面
- 递归: 分区后得到的数组左边都比基准值小,右边都比基准值大。然后递归的对基准值前后的子数组再进行分区
代码实现
const fastSort = (arr) => {
if(arr.length === 0) {return arr;}
// 定义左右两个数组,大于基准的放右边数组,小于基准的放左边基准
const left = []
const right = []
// 基准是随机挑选的一个数
const stand = arr[0]
// 因为数组第一个数被挑出来作为基准了,所以这里i从1开始
for(let i = 1; i < arr.length; i++){
if(arr[i] < stand){
left.push(arr[i])
}else{
right.push(arr[i])
}
}
// 这里左数组、右数组、基准为三个独立的部分。需要将三者合成一个数组
//...是ES6中新增的扩展运算符,数组中的扩展运算符(…)用于取出参数数组中的所有可遍历属性,拷贝到当前数组之中。
return [...fastSort(left),stand,...fastSort(right)]
}
const arr = [1,4,0,3,7]
console.log(fastSort(arr))
但是其实这种快速排序方法是错误的,虽然也是利用快速排序的原理左右分区然后递归,但是使用了两个数组,空间复杂度增加
下面因该是比较贴切一点的快速排序,可以使用console.time()方法来测试一下排序用时
// 计算程序执行时间
console.time('a')
// 函数传入三个参数,依次是:待排序的数组,待排序数组左边第一个元素的索引,待排序元素右边得第一个元素的索引
const fastSort = (arr,began,end) => {
// 取数组的第一个元素为基准值
let print = arr[began]
//如果不定义i,j 直接改变began,end的值,会影响后面递归开始和结束的索引值
let i = began;
let j = end;
//当左边的索引值大于右边的时,说明本轮排序结束
if(began >= end) {
// 将本轮排序后的数组返回
return arr
}
//把所有比基准值小的元素放在基准值左边,大的元素放在基准值右边
while(i < j){
// 从后往前查找到比基准值小的元素交换
//继续判断i<j是因为:如果print值较小,arr[j]>print会持续成立,导致j<=i,索引j<=i表示整个数组已经排序结束
while(arr[j] >= print && i < j){
j--
}
// 比基准值小的元素放左边,如果不存在比基准值小的元素,此时j=i相当于自身给自身赋值
arr[i] = arr[j]
// 找到比基准值大的元素交换
// 继续判断i<j,因为print值较大时,arr[i] <= print会持续成立,导致j<=i
while(arr[i] <= print && i < j){
i++
}
// 比基准值大的元素放在右边,如果不存在比基准值大的元素,此时i=j相当于自身给自身赋值
arr[j] = arr[i]
}
//一次循环结束,将基准值放在中间位置(此时i=j,arr[i]或者arr[j]都可以)
arr[j] = print
//递归,继续对基准值左右两边的数组进行排序
// 这里began不能直接传入0,会影响到第三轮之后的递归,
// 因为对基准值右边的数组再进行分区排序时也会使用到began,而此时began并不是0
fastSort(arr,began,j-1)
fastSort(arr,j+1,end)
//返回排序后的数组
return arr
}
const arr = [9,5,1,4,8]
console.log(fastSort(arr,0,arr.length-1))
console.timeEnd('a')
之所以从右边开始排序,是因为基准值选择的是最左边,举个例子7 1 2 3 4执行快速排序,假如从左开始排,就会出现1,7,2,3,4
附:这里有一个25w个数据的乱序数组(有重复),可以用来测试这些排序算法的速度 https://download.csdn.net/download/aaahuahua/34715763
2️⃣查找方法
搜索:js中通常使用indexOf方法(返回元素在数组中的下标,不存在返会-1)
一、顺序查找
- 遍历数组
- 找到和目标值相等的元素,返回下标
- 遍历所有值后如果没有目标值返回-1
代码实现
// 在Array数组原型上添加一个方法
Array.prototype.searchWay = function (item){
// this指代这个数组
for(let i = 0; i < this.length; i++){
if(item === this[i]){
return i
}
}
return -1
}
const arr = [1,3,5,2,9]
//调用searchWay方法
console.log(arr.searchWay(5))
二、二分查找
前提: 查找的是有序数组
如果是一个无序数组,需要先将其化为有序数组之后再进行二分搜索
搜索方法
- 从中间元素开始,如果正好是目标值,则搜索结束
- 如果目标值小于或者大于中间元素,则在大于或者小于目标值的部分数组中继续进行搜索
// 在数组的原型上添加一个search方法
Array.prototype.search = function (item){4
// 定义查找的起始位置和结束位置
let low = 0;
let height = this.length - 1;
// 等于号用于判断数组中只剩下最后一个数时,是否为目标值
while(low <= height){
// 选取数组中间元素为基准值
let mid = Math.floor((low + height) / 2);
const num = this[mid]
// 如果目标值就是基准值则返回基准值的下标
if(item === num){
return console.log(mid)
}else if(item > num){ //当目标值大于基准值时,将查找范围缩小为基准值右边的数组
low = mid + 1
}else if(item < num ){//当目标值小于基准值时,将查找范围缩小为基准值左边的数组
height = mid - 1
}
}
// 查找结束后如果还是没有找到目标元素,则返回该元素不存在
return console.log("该元素不存在")
}
//调用数组原型上的search方法
const arr = [1,2,3,4,5].search(0)
3️⃣算法设计思想
一、动态规划
将一个问题分解成相互重叠的子问题,通过反复求解子问题,来解决原来的问题
步骤:
- 定义子问题
- 循环执行子问题中定义的公式
实战练习
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1 阶 + 1 阶 + 1 阶
1 阶 + 2 阶
2 阶 + 1 阶
动态规划
- 定义子问题,到达第n阶可以有两种方法
(1)从第n-1阶爬一阶
(2)从第n-2阶爬两阶
所以到达第n阶的方法就是到达第n-1阶的方法加上到达第n-2阶方法之和:F(n)=F(n-1)+F(n-2)
- 从n=2开始循环执行F(n)=F(n-1)+F(n-2)
代码实现
var climbStairs = function(n) {
//当阶数小于2,也就是1阶时,直接返回1(n为正整数所以不存在复数的情况)
if(n<2) return 1;
//定义数组dp存放到达每一阶的方法数,下标代表阶数,对应值为到达改阶的方法数
//虽然这里0阶定义的是1,但是n为正整数,不会存在n=0的情况
const dp = [1,1];
//因为前两次已经被列举出来了,所以i从2开始
for(let i = 2;i <= n; i++){
dp[i]=dp[i-1]+dp[i-2]
}
return dp[n]
};
//代码优化,因为定义了数组,所以空间复杂度为O(n),可以只定义两个变量将复杂度降到O(1)
var climbStairs = function(n) {
if(n<2) return 1;
const dp0 = 1;
const dp1 = 1;
for(let i = 2;i <= n; i++){
//临时存储到达第n-2阶的方法
const temp = dp0;
dp0 = dp1;
dp1 = temp + dp1;
}
return dp1;
};
二、分而治之
将原问题分为很多个相似的小问题,递归的解决这些小问题,然后再将结果合并以解决原有的问题
1. 设计案例之归并排序
- 分:将数组一分为二
- 解:递归的对两个子数组进行归并排序
- 合:合并有序数组(将递归后长度为1的数组合并成有序数组,然后再把这些有序数组继续合并)
2. 设计案例之快速排序
- 分:选择基准值将原数组分成大于基准值和小于基准值的两个数组
- 解:递归的对两个子数组进行快速排序
- 合:递归到数组中只剩下一个·元素时排序完毕,返回该数组即可
实战练习
猜数字游戏的规则如下:
每轮游戏,都会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。
你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):
- -1:选出的数字比你猜的数字小 pick < num
- 1:选出的数字比你猜的数字大 pick > num
- 0:选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num 返回选出的数字。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/guess-number-higher-or-lower
示例 1: 输入:n = 10, pick = 6 输出:6
示例 2:输入:n = 1, pick = 1 输出:1
示例 3:输入:n = 2, pick = 1 输出:1
示例 4:输入:n = 2, pick = 2 输出:2
解题思路:
使用二分搜索,分,解,和,利用分而治之的思想来做
- 分:计算中间元素,分割数组
- 解:递归的在较大的或者较小的数组中进行二分搜索
- 合:这一步可以省略,因为如果在子数组中搜索到目标值后就直接将其返回了
代码实现:
var guessNumber = function(n) {
//这里定义一个二分查找的函数
const divide = (low,height) => {
//当最小值大于最大值时直接return
if(low > height) return;
//取两个数的中间值
const mid = Math.floor((low+height)/2);
//调用接口判断猜测结果是否正确
const res = guess(mid);
if(res === 0 ){
return mid
}else if(res === -1){
//猜测结果小于真实值
return divide(1,mid-1)
}else if(res === 1 ){
//猜测结果大于真实值
return divide(mid+1,height)
}
}
return divide(1,n)
};
对称二叉树
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/symmetric-tree
解题思路:
可以转换为左右子树是否为镜像的,然后分解为树1的左子树和树2的右子树是否为互为镜像,树1右子树和树2的左子树是否为互为镜像
代码实现:
var isSymmetric = function(root) {
//如果二叉树为空,返回为true
if(!root) return true;
//定义一个镜像判断函数
const isCheck = (left,right) => {
//如果左右子树都不存在,返回为true
if(!right && !left){
return true
}
//左右子树存在,值相等,递归左右子节点
if(right && left && right.val === left.val &&
isCheck(left.left,right.right) &&
isCheck(left.right,right.left)
){
return true
}
//其他情况下返回false
return false
}
return isCheck(root.left,root.right)
};
三、贪心算法
期盼通过每个阶段的局部最优选择,从而达到全局的最优。但是结果有可能并不是最优的
分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/assign-cookies
示例1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释: 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 所以你应该输出1。
示例2:
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.
解题思路:
局部最优:分发的饼干既能满足孩子,还消耗最少,即先将较小的饼干分给胃口小的孩子
解题步骤:
- 对饼干数组和胃口数组进行升序排序
- 遍历饼干数组,找到能满足第一个孩子(胃口最小的孩子)的饼干
- 继续遍历饼干数组,找到剩下能满足第二、第三、第四…个孩子的饼干
代码实现:
var findContentChildren = function(g,s) {
//快速排序
const fastSort = (arr,began,end) => {
let print = arr[began]
let i = began;
let j = end;
if(began >= end){
return arr;
}
while(i < j){
while(arr[j] >= print && i < j){
j--
}
arr[i] = arr[j]
while(arr[i] <= print && i < j){
i++
}
arr[j] = arr[i];
}
arr[j] = print;
fastSort(arr,began,j-1)
fastSort(arr,j+1,end)
return arr
}
//对小孩的胃口和饼干尺寸两个数组进行排序
fastSort(s,0,s.length-1)
fastSort(g,0,g.length-1)
//定义能满足的小孩个数
let i = 0;
//遍历所有饼干
s.forEach(item =>{
//如果饼干尺寸大于小孩的胃口,能满足的小孩数加1
if(item >= g[i]){
i+=1
}
})
//返回满足的小孩个数
return i
};
四、回溯算法
一种渐进式寻找并构建问题解决方式的策略,即从一个可能的情况开始解决问题,如果不行,就回溯到另一种情况,直到问题被解决(也可以理解为暴力破解)
回溯思路
- 使用递归模拟出所有的情况
- 遇到不满足条件的情况就回溯
- 收集所有能到达递归终点的情况,并返回
实战练习
全排列规则如下:
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序返回答案。
示例 1:输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/permutations
代码实现
var permute = function(nums) {
//定义结果集
let res = []
//定义已排列结果
let way = []
//used(标记数组),用于记录当前该元素是否已经被添加到已排列数组(way)中了
const backtrack = (arr,used) =>{
//递归结束条件,当已排列数组的长度等于原数组长度时,表明所有元素都已经被添加到已排列数组中了
if(way.length === arr.length) {
//将本次排列的结果添加到结果集中
res.push([...way])
return;
}
// 遍历数组
for(let i = 0; i < arr.length; i++){
// if(way.indexOf(arr[i]) != -1)
// 不使用这个方法是因为查找的时间复杂度为O(n),同样的引入used数组会增加空间复杂度也是O(n)
// undefind转换为布尔值是false
if(used[i]) // 判断该元素是否已经存在于已排列数组中
continue;
//向结果集中添加元素
way.push(arr[i])
//标记该元素为已添加,使下一层遍历能够区分哪些元素是未排列的剩余元素
used[i] = true
// 递归遍历数组,递归遍历arr.length次就可以将数组中的元素全部添加到排列数组中
backtrack(arr,used)
// 这个是本次全排列中回溯的重要体现
// 使用pop方法删除已经添加到排列数组中的元素,可以使排列过程回到上一层递归
way.pop()
// 删除已经添加到排列数组中的元素标记
used[i] = false
}
}
backtrack(nums,[])
return res
};
子集规则如下
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集不能包含重复的子集。你可以按任意顺序返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/subsets
代码实现
const subsets = (nums) => {
const res = [];
const sun = (index, temp) => {
if (index == nums.length) { // 索引等于数组长度时
res.push(temp.slice()); // 加入解集
return; // 结束当前的递归
}
temp.push(nums[index]); // 将该元素加入数组
sun(index + 1, temp); // 往下递归,添加剩余元素
temp.pop(); // 上面的递归结束,逐步删除已经添加过的元素
// 删除已添加的元素后,将剩余结果加入结果集;操作剩余元素
sun(index + 1, temp);
};
sun(0, []);
return res;
};