递归
- 把大的问题分解成为若干子问题,只考虑子问题和总问题的关系
- 想清楚 递归公式 和 递归的终止条件
- 递归的问题
- 会出现堆栈溢出的情况
- 空间复杂度高,函数调用耗时多
- 重复计算
为了避免重复计算, 我们可以使用散列表来优化斐波那契数列的计算
// 递归与散列表计算斐波那契数列
public int f(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
// hasSolvedList可以理解成一个Map,key是n,value是f(n)
if (hasSolvedList.containsKey(n)) {
return hasSolvedList.get(n);
}
int ret = f(n-1) + f(n-2);
hasSolvedList.put(n, ret);
return ret;
}
链表⭐
链表这个知识点在网络上有很多讲解, 这里仅仅提供一些leetcode刷题记录
L206 反转链表
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let p = head
let reverse = null
while (p!==null) {
// reverse移动, p移动, node作为临时节点
const node = p
p = p.next
node.next = reverse
reverse = node
}
return reverse
};
L92 指定位置反转链表
上述题目的进阶版, 主要加了一个截断链表的操作
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} left
* @param {number} right
* @return {ListNode}
*/
var reverse = function(link) {
let p = link
let reverse = null
while (p!==null) {
const node = p
p = p.next
node.next = reverse
reverse = node
}
return reverse
}
var reverseBetween = function(head, left, right) {
let beforeN = null, N=null, M=null, afterM=null
let dummy = new ListNode(-1, head)
let cur = dummy
for (let i=0; i<left-1; i++){
cur = cur.next
}
beforeN = cur
N = cur.next
for (let i=0; i<right-left+1; i++){
cur = cur.next
}
M = cur
afterM = cur.next
// 截断链表
beforeN.next = null
M.next = null
reverse(N)
beforeN.next = M
N.next = afterM
return dummy.next
};
L160 相交链表
// 暴力解法
function getIntersectionNode (headA, headB) {
const cacheA = new Set()
while (headA !== null) {
cacheA.add(headA)
headA = headA.next
}
while (headB !== null) {
if (cacheA.has(headB)) return headB
headB = headB.next
}
return null
}
(6 封私信 / 77 条消息) 想见你:教你如何用浪漫的方法找到两个单链表的相交结点 - 知乎 (zhihu.com)
// 优雅解法
function getIntersectionNode (headA, headB) {
let pA = headA
let pB = headB
while (pA!==pB) {
pA = (pA===null) ? headB : pA.next
pB = (pB===null) ? headA : pB.next
}
return pA
}
栈⭐
- 栈的本质是FILO, 类似于一个羽毛球筒
- 也类似于人类解决问题的思路, 即:引入大问题->解决其中的小问题->解决大问题 的思维模式
括号匹配
- 栈适合处理具有完全包含关系的问题, 而(())正是一种完全包含关系的问题
- 一对括号代表一组出栈与入栈的操作, 像是( ()() )可以理解为一个大事件解决之前, 要先解决两个小事件, 而大事件一定先发生, 后解决, 这正是FILO的思想
bool isValid(char *s) {
int32_t lnum = 0, rnum = 0;
int32_t len = strlen(s);
for (int32_t i=0; i<len; i++){
switch (s[i]) {
case '(' : ++lnum; break;
case ')' : ++rnum; break;
defalut : return false;
}
if (lnum>=rnum) continue;
return false;
}
return lnum==rnum;
}
中缀转后缀表达式
数据结构---前缀 中缀 后缀 表达式之间的转换_哔哩哔哩_bilibili
队列
LEETCODE : 20,155,232,844,224,682,496
广义表
【数据结构】两分钟熟练掌握广义表表头表尾计算!_哔哩哔哩_bilibili
二叉树
二叉树中度为1的节点, 要区分左孩子和右孩子; 二叉树是一种递归概念, 它可以表示成如下五种形态
此外, 二叉树可以分为满二叉树
以及完全二叉树
排序算法⭐
冒泡排序
// 冒泡排序
let arr = [2, 5, 3, 7, 6, 4, 21, 13, 1]
function bubbleSort (nums, flag) {
let length = nums.length
for (let i = 0; i < length; i++) {
let flag = false
for (let j = 0; j < length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
flag = true
}
}
// 如果在内层循环一次后一次交换都没有发生, 直接停止外层循环
if (!flag) {
break
}
}
return (flag === 1) ? nums : nums.reverse()
}
console.log(bubbleSort(arr, 1))
插入排序
function insertSort (arr) {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = i + 1; j > 0; j--) {
if (arr[j - 1] > arr[j]) {
[arr[j - 1], arr[j]] = [arr[j], arr[j - 1]]
}
}
}
return arr
}
let arr = [2, 5, 3, 7, 6, 4, 21, 13, 1]
console.log(insertSort(arr))
希尔排序
function shellSort (arr) {
let len = arr.length
// gap 即为增量
for (let gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
for (let i = gap; i < len; i++) {
let j = i
let current = arr[i]
while (j - gap >= 0 && current < arr[j - gap]) {
arr[j] = arr[j - gap]
j = j - gap
}
arr[j] = current
}
}
return arr
}
var arr = [3, 5, 7, 1, 4, 56, 12, 78, 25, 0, 9, 8, 42, 37]
console.log(shellSort(arr))
选择排序
let arr = [2, 5, 3, 7, 6, 4, 21, 13, 1]
function selectSort (arr) {
let i = 0
while (i < arr.length - 1) {
let MinIndex = i
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[MinIndex]) {
MinIndex = j
}
}
[arr[i], arr[MinIndex]] = [arr[MinIndex], arr[i]]
i++
}
return arr
}
console.log(selectSort(arr))
console.log(selectSort(arr).reverse())
快速排序
快速排序是基于递归和分治的思想实现的
排序流程:
- 先取出数组中的一个值, 作为基准值
- 根据这个基准值, 再分为小于基准值的数组和大于基准值的数组
- 进入递归
- 当递归的数组小于两个时, 跳出递归, 将这个数组返回基线条件
- 最后将每个数组拼接在一起
代码实现:
const arr = [11, 2, 4, 3, 5, 87]
function quickSort(arr) {
if (arr.length<2) {
return arr
}
let goal = arr[0]
let minArr = arr.slice(1).filter(item=>item<=goal)
let maxArr = arr.slice(1).filter(item=>item>goal)
return quickSort(minArr).concat([goal]).concat(quickSort(maxArr))
}
console.log(quickSort(arr))
计数排序
算法思想:
- 找到数组中最大的元素
- 使用一个数组, 长度等于最大值max+1(数字从0开始),并记录每个元素出现的次数
- 根据计数数组, 依次输出到新的数组中
function countSort (arr) {
let countArrayLen = Math.max(...arr) + 1
let countArray = new Array(countArrayLen).fill(0)
// 把原数组中的num作为计数数组的索引值, 记录下来
for (let num of arr) {
countArray[num]++
}
let res = []
for (let i = 0; i < countArray.length; i++) {
while (countArray[i] > 0) {
res.push(i)
countArray[i]--
}
}
return res
}
let arr = [2, 5, 3, 7, 6, 4, 21, 13, 1]
console.log(countSort(arr))
桶排序
与计数排序的比较:
- 桶排序是计数排序的拓展版本
- 计数排序可以看成每个桶只存放一个元素, 桶排序的每个桶需要存放一定范围内的元素
- 最后都要开辟一个新空间用来存放结果
function bucketSort (arr) {
let MaxNum = Math.max(...arr)
let MinNum = Math.min(...arr)
// 获取桶的长度
let bucketLen = Math.floor((MaxNum - MinNum) / arr.length) + 1
// bucketArr是一个二维数组, 一开始给每个元素填充上一个空数组
let bucketArr = new Array(bucketLen).fill(null).map(_ => [])
// 遍历原数组, 将每个元素放入桶数组中
for (let num of arr) {
let index = Math.floor((num - MinNum) / arr.length)
bucketArr[index].push(num)
}
const res = []
for (let item of bucketArr) {
// bucketArr中的每个元素都是一个数组, 先对它的每一个元素进行排序
item.sort((a, b) => a - b)
// 依次放入result数组中
for (let num of item) {
res.push(num)
}
}
return res
}
let arr = [2, 5, 3, 7, 6, 4, 21, 13, 1]
console.log(bucketSort(arr))
基数排序
基数排序(Radix Sort)是桶排序的拓展, 它的基本思想是将整数按位数切割成不同的数字, 然后按照每个位进行比较
具体做法是: 将所有待比较数值统一为相同的数位长度, 数位较短的前面补0, 然后, 从最低位开始, 依次进行依次排序, 这样从最低位一直到最高位排序后, 就变成了一个有序数列
//LSD Radix Sort
var arr = [2, 5, 3, 7, 6, 4, 21, 13, 1]
var counter = []
function radixSort (arr, maxDigit) {
var mod = 10
var dev = 1
for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
for (var j = 0; j < arr.length; j++) {
var bucket = parseInt((arr[j] % mod) / dev)
if (counter[bucket] == null) {
counter[bucket] = []
}
counter[bucket].push(arr[j])
}
var pos = 0
for (var j = 0; j < counter.length; j++) {
var value = null
if (counter[j] != null) {
while ((value = counter[j].shift()) != null) {
arr[pos++] = value
}
}
}
}
return arr
}
console.log(radixSort(arr, 2))
归并排序
当遇到一个很大的难题时,我们常常想到的是,能不能把这个事情拆成很多块儿,然后一块儿一块的解决它,最后合起来,这个大难题不就解决了嘛。
也就是我们常说的 ”分而治之“,归并排序可以说是将 分而治之 的思想表现得淋漓尽致。什么意思呢,来看思路,现在有一个数组 arr 需要排序:
- 假如说这个数组的左边一半
arr_left是有序的,右边一半arr_right也是有序的,整体不是有序的。那么将arr_left和arr_right做一次归并操作后,是不是最终就能得到一个有序的数组呀! - 又假如,数组的左边一半
arr_left它整体是无序的,但是它的左边一半arr_left_left是有序的,并且它的右边一半arr_left_right是有序的。哎,那将arr_left_left和arr_left_right做一次归并操作后,是不是就能得到一个有序的数组呀,也就是arr_left就有序了呀 - 是不是就是一个拆分的过程呀,将大数组拆分成小数组,然后一直假设下去...
- 直到什么时候呢,直到这个小数组只有一个元素了,只有一个元素了是不是就代表着这个数组有序了呀,然后再一层一层的做归并操作,每做一次归并操作,两个小数组就能合成一个大的有序数组
- 最终,这个大数组是不是就能有序了呀
总结:”分而治之“,要将一个数组排序,可以先(递归地)将数组分成两半排序,然后再依次归并起来。
整体的思路如下:
// 合并两个有序数组(在两个数组上归并)
function merge_two_sortedArr (arr1, arr2) {
const l1 = arr1.length
const l2 = arr2.length
let p = 0
let q = 0
let res = []
while (p < l1 || q < l2) {
if (p === l1) {
res.push(arr2[q++])
} else if (q === l2) {
res.push(arr1[p++])
} else if (arr1[p] < arr2[q]) {
res.push(arr1[p++])
} else {
res.push(arr2[q++])
}
}
return res
}
let arr1 = [1, 4, 6]
let arr2 = [3, 12, 88]
console.log(merge_two_sortedArr(arr1, arr2))
// 在原数组上进行归并
function merge_in_place (arr, l, mid, r) {
let keep = []
for (let i = l; i <= r; i++) {
keep[i] = arr[i]
}
let p = l
let q = mid + 1
for (let i = l; p <= mid || q <= r; i++) {
if (p === mid + 1) {
arr[i] = keep[q++]
} else if (q === r + 1) {
arr[i] = keep[p++]
} else if (keep[p] < keep[q]) {
arr[i] = keep[p++]
} else {
arr[i] = keep[q++]
}
}
return keep
}
let arr = [1, 4, 5, 2, 3, 6, 7]
merge_in_place(arr, 0, 2, arr.length - 1)
console.log(arr)
// 归并排序
function merge (arr, l, mid, r) {
const keep = []
for (let i = l; i <= r; i++) {
keep[i] = arr[i]
}
for (let p = l, q = mid + 1, k = l; p <= mid || q <= r;) {
if (p === mid + 1) arr[k++] = keep[q++]
else if (q === r + 1) arr[k++] = keep[p++]
else if (keep[p] > keep[q]) arr[k++] = keep[q++]
else arr[k++] = keep[p++]
}
}
function mSort (arr, l, r) {
if (l >= r) return
// 分半
const mid = l + ((r - l) >> 1)
// 归并排序左侧
mSort(arr, l, mid)
// 归并排序右侧
mSort(arr, mid + 1, r)
// 走到这里, l~mid 部分已排序,mid+1~r 部分已排序
// 直接原地归并操作就可以实现数组排序
merge(arr, l, mid, r)
}
function MergeSort (arr) {
mSort(arr, 0, arr.length - 1)
}
const nums = [12, 1, 7, 4, 5, 2, 10, 6, 3, 11, 9, 8, 13]
MergeSort(nums)
console.log(nums);
动态规划
保证最优解
贪心
快速找到比较优的解
Javascript源码
- Javascript里的sort是什么排序?
- v8里当数组长度小于10用插入排序, 否则使用快速排序
- 现版本采用Timsort排序, 它源于归并排序和插入排序, 是一种混合与稳定的排序算法