算法知识总结
本部分主要是笔者在学习算法知识和一些相关面试题所做的笔记,如果出现错误,希望大家指出!
目录
- 常用算法和数据结构总结
- 排序
- 树
- 链表
- 动态规划
- 经典笔试题
- 1. js 实现一个函数,完成超过范围的两个大整数相加功能
- 2. js 如何实现数组扁平化?
- 3. js 如何实现数组去重?
- 4. 如何求数组的最大值和最小值?
- 5. 如何求两个数的最大公约数?
- 6. 如何求两个数的最小公倍数?
- 7. 实现 IndexOf 方法?
- 8. 判断一个字符串是否为回文字符串?
- 9. 实现一个累加函数的功能比如 sum(1,2,3)(2).valueOf()
- 10. 使用 reduce 方法实现 forEach、map、filter
- 11. 设计一个简单的任务队列,要求分别在 1,3,4 秒后打印出 “1”, “2”, “3”
- 12. 如何查找一篇英文文章中出现频率最高的单词?
- 常见面试智力题总结
- 1. 时针与分针夹角度数问题?
- 2. 用3升,5升杯子怎么量出4升水?
- 3. 浑浊药罐问题
- 4. 卡片证明问题
- 5. 赛马问题,25 匹马,5 个赛道,最少几次能选出最快的三匹马?
- 6. 五队夫妇参加聚会握手问题
- 7. 你只能带行走 60 公里的油,只能在起始点加油,如何穿过 80 公里的沙漠?
- 8. 烧一根不均匀的绳要用一个小时,如何用它来判断一个小时十五分钟?
- 9. 有7克、2克砝码各一个,天平一只,如何只用这些物品三次将140克的盐分成50、90克各一份?
- 10. 火车相对而行,小鸟飞行距离问题
- 11. 弹球拾取几率问题
- 12. 8个球使用天平称重问题
- 13. 三盏灯区分开关问题
- 14. 盲人黑白袜子问题
- 15. 水果标签问题
- 16. 一个班级60%喜欢足球,70%喜欢篮球,80%喜欢排球,问即三种球都喜欢占比有多少?
- 17. 五只鸡五天能下五个蛋,一百天下一百个蛋需要多少只鸡?
- 剑指 offer 思路总结
- 题目
- 1. 二维数组中的查找
- 2. 替换空格
- 3. 从尾到头打印链表
- 4. 重建二叉树
- 5. 用两个栈实现队列
- 6. 旋转数组的最小数字
- 7. 斐波那契数列
- 8. 跳台阶
- 9. 变态跳台阶
- 10. 矩形覆盖
- 11. 二进制中1的个数
- 12. 数值的整数次方
- 13. 调整数组顺序使奇数位于偶数前面
- 14. 链表中倒数第 k 个节点
- 15. 反转链表
- 16. 合并两个排序的链表
- 17. 树的子结构
- 18. 二叉树的镜像
- 19. 顺时针打印矩阵
- 20. 定义一个栈,实现 min 函数
- 21. 栈的压入弹出
- 22. 从上往下打印二叉树
- 23. 二叉搜索树的后序遍历
- 24. 二叉树中和为某一值路径
- 25. 复杂链表的复制
- 26. 二叉搜索树与双向链表
- 27. 字符串的排列
- 28. 数组中出现次数超过一半的数字
- 29. 最小的 K 个数
- 30. 连续子数组的最大和
- 31. 整数中1出现的次数(待深入理解)
- 32. 把数组排成最小的数
- 33. 丑数(待深入理解)
- 34. 第一个只出现一次的字符
- 35. 数组中的逆序对
- 36. 两个链表的第一个公共结点
- 37. 数字在排序数组中出现的次数
- 38. 二叉树的深度
- 39. 平衡二叉树
- 40. 数组中只出现一次的数字
- 41. 和为 S 的连续正数序列
- 42. 和为 S 的两个数字
- 43. 左旋转字符串
- 44. 翻转单词顺序列
- 45. 扑克牌的顺子
- 46. 圆圈中最后剩下的数字(约瑟夫环问题)
- 47. 1 2 3 … n
- 48. 不用加减乘除做加法
- 49. 把字符串转换成整数。
- 50. 数组中重复的数字
- 51. 构建乘积数组
- 52. 正则表达式的匹配
- 53. 表示数值的字符串
- 54. 字符流中第一个不重复的字符
- 55. 链表中环的入口结点
- 56. 删除链表中重复的结点
- 57. 二叉树的下一个结点
- 58. 对称二叉树
- 59. 按之字形顺序打印二叉树(待深入理解)
- 60. 从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
- 61. 序列化二叉树(待深入理解)
- 62. 二叉搜索树的第 K 个节点
- 63. 数据流中的中位数(待深入理解)
- 64. 滑动窗口中的最大值(待深入理解)
- 65. 矩阵中的路径(待深入理解)
- 66. 机器人的运动范围(待深入理解)
- 相关算法题
- 题目
常用算法和数据结构总结
排序
冒泡排序
冒泡排序的基本思想是,对相邻的元素进行两两比较,顺序相反则进行交换,这样,每一趟会将最小或最大的元素“浮”到顶端,
最终达到完全有序。
代码实现:
function bubbleSort(arr) {
if (!Array.isArray(arr) || arr.length <= 1) return;
let lastIndex = arr.length - 1;
while (lastIndex > 0) {
// 当最后一个交换的元素为第一个时,说明后面全部排序完毕
let flag = true, k = lastIndex;
for (let j = 0; j < k; j++) {
if (arr[j] > arr[j + 1]) {
flag = false;
lastIndex = j; // 设置最后一次交换元素的位置
[arr[j], arr[j+1]] = [arr[j+1], arr[j]];
}
}
if (flag) break;
}
}
冒泡排序有两种优化方式。
一种是外层循环的优化,我们可以记录当前循环中是否发生了交换,如果没有发生交换,则说明该序列已经为有序序列了。
因此我们不需要再执行之后的外层循环,此时可以直接结束。
一种是内层循环的优化,我们可以记录当前循环中最后一次元素交换的位置,该位置以后的序列都是已排好的序列,因此下
一轮循环中无需再去比较。
优化后的冒泡排序,当排序序列为已排序序列时,为最好的时间复杂度为 O(n)。
冒泡排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,是稳定排序。
详细资料可以参考:
《图解排序算法(一)》
《常见排序算法 - 鸡尾酒排序 》
《前端笔试&面试爬坑系列—算法》
《前端面试之道》
选择排序
选择排序的基本思想为每一趟从待排序的数据元素中选择最小(或最大)的一个元素作为首元素,直到所有元素排完为止。
在算法实现时,每一趟确定最小元素的时候会通过不断地比较交换来使得首位置为当前最小,交换是个比较耗时的操作。其实
我们很容易发现,在还未完全确定当前最小元素之前,这些交换都是无意义的。我们可以通过设置一个变量 min,每一次比较
仅存储较小元素的数组下标,当轮循环结束之后,那这个变量存储的就是当前最小元素的下标,此时再执行交换操作即可。
代码实现:
function selectSort(array) {
let length = array.length;
// 如果不是数组或者数组长度小于等于1,直接返回,不需要排序
if (!Array.isArray(array) || length <= 1) return;
for (let i = 0; i < length - 1; i++) {
let minIndex = i; // 设置当前循环最小元素索引
for (let j = i + 1; j < length; j++) {
// 如果当前元素比最小元素索引,则更新最小元素索引
if (array[minIndex] > array[j]) {
minIndex = j;
}
}
// 交换最小元素到当前位置
// [array[i], array[minIndex]] = [array[minIndex], array[i]];
swap(array, i, minIndex);
}
return array;
}
// 交换数组中两个元素的位置
function swap(array, left, right) {
var temp = array[left];
array[left] = array[right];
array[right] = temp;
}
选择排序不管初始序列是否有序,时间复杂度都为 O(n²)。
选择排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,不是稳定排序。
详细资料可以参考:
《图解排序算法(一)》
插入排序
直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。
插入排序核心–扑克牌思想: 就想着自己在打扑克牌,接起来一张,放哪里无所谓,再接起来一张,比第一张小,放左边,
继续接,可能是中间数,就插在中间…依次
代码实现:
function insertSort(array) {
let length = array.length;
// 如果不是数组或者数组长度小于等于1,直接返回,不需要排序
if (!Array.isArray(array) || length <= 1) return;
// 循环从 1 开始,0 位置为默认的已排序的序列
for (let i = 1; i < length; i++) {
let temp = array[i]; // 保存当前需要排序的元素
let j = i;
// 在当前已排序序列中比较,如果比需要排序的元素大,就依次往后移动位置
while (j -1 >= 0 && array[j - 1] > temp) {
array[j] = array[j - 1];
j--;
}
// 将找到的位置插入元素
array[j] = temp;
}
return array;
}
当排序序列为已排序序列时,为最好的时间复杂度 O(n)。
插入排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,是稳定排序。
详细资料可以参考:
《图解排序算法(一)》
希尔排序
希尔排序的基本思想是把数组按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的元
素越来越多,当增量减至1时,整个数组恰被分成一组,算法便终止。
代码实现:
function hillSort(array) {
let length = array.length;
// 如果不是数组或者数组长度小于等于1,直接返回,不需要排序
if (!Array.isArray(array) || length <= 1) return;
// 第一层确定增量的大小,每次增量的大小减半
for (let gap = parseInt(length >> 1); gap >= 1; gap = parseInt(gap >> 1)) {
// 对每个分组使用插入排序,相当于将插入排序的1换成了 n
for (let i = gap; i < length; i++) {
let temp = array[i];
let j = i;
while (j - gap >= 0 && array[j - gap] > temp) {
array[j] = array[j - gap];
j -= gap;
}
array[j] = temp;
}
}
return array;
}
希尔排序是利用了插入排序对于已排序序列排序效果最好的特点,在一开始序列为无序序列时,将序列分为多个小的分组进行
基数排序,由于排序基数小,每次基数排序的效果较好,然后在逐步增大增量,将分组的大小增大,由于每一次都是基于上一
次排序后的结果,所以每一次都可以看做是一个基本排序的序列,所以能够最大化插入排序的优点。
简单来说就是,由于开始时每组只有很少整数,所以排序很快。之后每组含有的整数越来越多,但是由于这些数也越来越有序,
所以排序速度也很快。
希尔排序的时间复杂度根据选择的增量序列不同而不同,但总的来说时间复杂度是小于 O(n^2) 的。
插入排序是一个稳定排序,但是在希尔排序中,由于相同的元素可能在不同的分组中,所以可能会造成相同元素位置的变化,
所以希尔排序是一个不稳定的排序。
希尔排序的平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(n^s) ,空间复杂度为 O(1) ,不是稳定排序。