算法
两个指标:
- 时间复杂度 T(n)
- 空间复杂度 S(n)
时间复杂度上界 O(f(n))
下界 Ω(f(n))
T1(n) + T2(n) = max(O(f1(n)), O(f2(n)))
T1(n) × T2(n) = O(f1(n) × f2(n))
for 循环的时间复杂度 = 循环次数 × 循环体代码的复杂度
if-else 结构的复杂度 = max(if 的条件判断复杂度,分支一的复杂度,分支二的复杂度)
线性表
由同类型数据元素构成有序序列的线性结构
- 表中元素个数称为线性表的长度
- 线性表没有元素时,称为空表
- 表的起始位置称为表头,表结束位置称为表尾
顺序储存:
- 类似数组 splice 的添加删除操作,要求物理与逻辑统一
链式储存:
- 不要求逻辑上相邻的两个元素物理上也相邻,通过 “链” 建立起数据元素之间的逻辑关系
广义表:
- 是线性表的推广
- 对线性表而言,n 个元素都是基本的单元素
- 广义表中,这些元素不仅可以是单元素,也可以是另一个广义表
多重链表:
- 多重链表中节点的指针域会有多个
- 但包含两个指针域的链表并不一定是多重链表,比如双向链表不是多重链表
堆栈:
- 具有一定操作约束的线性表
- 只在一端做插入、删除,后入先出(LIFO)
- 堆栈应用很广,如下的后缀表达式计算便是利用了堆栈思想
// 中缀表达式转为后缀表达式
栈的顺序储存实现:
- 栈的顺序储存结构通常由一个一维数组和一个记录栈顶元素位置的变量组成
栈的链表储存实现:
- 栈的链式储存结构实际上就是一个单链表,叫做链栈。插入和删除操作只能再链栈的栈顶进行,栈顶指针 Top 只能在链表的头部,而不是尾部
二叉树
逻辑上二叉树有五种基本形态:
- 空二叉树——如图 a
- 只有一个根结点的二叉树——如图 b
- 只有左子树——如图 c
- 只有右子树——如图 d
- 完全二叉树——如图 e
类型:
- 完全二叉树——若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。
- 满二叉树——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。
- 平衡二叉树——平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
相关术语:
树的结点(node):包含一个数据元素及若干指向子树的分支;
孩子结点(child node):结点的子树的根称为该结点的孩子;
双亲结点:B 结点是A 结点的孩子,则A结点是B 结点的双亲;
兄弟结点:同一双亲的孩子结点; 堂兄结点:同一层上结点;
祖先结点: 从根到该结点的所经分支上的所有结点
子孙结点:以某结点为根的子树中任一结点都称为该结点的子孙
结点层:根结点的层定义为1;根的孩子为第二层结点,依此类推;
树的深度:树中最大的结点层
结点的度:结点子树的个数
树的度: 树中最大的结点度。
叶子结点:也叫终端结点,是度为 0 的结点;
分枝结点:度不为0的结点;
有序树:子树有序的树,如:家族树;
无序树:不考虑子树的顺序;
二叉树的性质:
- 在二叉树的第i层上至多有2的i-1次方个结点(i>=1)。
- 深度为k的二叉树至多有2的k次方-1个结点,(k>=1)。
- 对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2 + 1;
一棵深度为k且有2的k次方-1个结点的二叉树称为满二叉树。
深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树。
下面是完全二叉树的两个特性:
- 具有n个结点的完全二叉树的深度为Math.floor(log 2 n) + 1
- 如果对一棵有n个结点的完全二叉树(其深度为Math.floor(log 2 n) + 1)的结点按层序编号(从第1层到第Math.floor(2 n) + 1,每层从左到右),则对任一结点(1<=i<=n)有:
- 如果i=1,则结点i、是二叉树的根,无双亲;如果i>1,则其双亲parent(i)是结点Math.floor(i/2)。
- 如果2i > n,则结点i无左孩子(结点i为叶子结点);否则其左孩子LChild(i)是结点2i.
- 如果2i + 1 > n,则结点i无右孩子;否则其右孩子RChild(i)是结点2i + 1;
二叉树遍历:
L:left —— D:根节点 —— R:right
前序遍历(DLR):FCADBEGHP
中序遍历(LDR):ACBDFEHGP
后序遍历(LRD):ABDCHPGEF
用 for 循环代替递归
递归会增加空间复杂度,如果传入数值过大会导致程序爆掉
// 时间测试函数
function timer(fn) {
console.time(fn.name)
fn(10000)
console.timeEnd(fn.name)
}
// 递归
function fun2(n) {
if (n) fun2(n - 1)
return
}
// for 循环
function fun1(n) {
for (let i = 0; i <= n; i++) { }
}
timer(console.log)
timer(fun2) // fun2: 1.062ms
timer(fun1) // fun1: 0.317ms
加减法运算远快于乘除法运算
算法题
给定 N 个整数的序列 { A1,A2,…,An },求函数
f(i,j)=max{0,ΣAk}
的最大值
// 复杂度 O(n^3)
function maxSubSeqSum(arr) {
let thisSum, maxSum = 0, len = arr.length
for (let i = 0; i < len; i++) {
for (let j = i; j < len; j++) {
thisSum = 0
for (let k = i; k <= j; k++)
thisSum += arr[k]
if (thisSum > maxSum)
maxSum = thisSum
}
}
return maxSum
}
let arr = [1, 2, 3, 0, -2, 4, 9, - 3]
console.log(maxSubSeqSum(arr)); // 17
改进
// 复杂度 O(n^2)
function maxSubSeqSum(arr) {
let thisSum, maxSum = 0, len = arr.length
for (let i = 0; i < len; i++) {
thisSum = 0
for (let j = i; j < len; j++) {
thisSum += arr[j]
if (thisSum > maxSum)
maxSum = thisSum
}
}
return maxSum
}
let arr = [1, 2, 3, 0, -2, 4, 9, - 3]
console.log(maxSubSeqSum(arr)); // 17
最优解
// 复杂度 O(n)
function maxSubSeqSum(arr) {
let thisSum = 0, maxSum = 0
for (let i = 0, len = arr.length; i < len; i++) {
thisSum += arr[i]
if (thisSum > maxSum) maxSum = thisSum
else if (thisSum < 0) thisSum = 0 // 当前子列和为负则抛弃
}
return maxSum
}
爬楼梯问题
有一楼梯共M级,刚开始时你在第一级,若每次只能跨上一级或二级,要走上第M级,共有多少种走法?
分析: 这个问题要倒过来看,要到达n级楼梯,只有两种方式,从(n-1)级 或 (n-2)级到达的。所以可以用递推的思想去想这题,假设有一个数组s[n], 那么s[1] = 1(由于一开始就在第一级,只有一种方法), s[2] = 1(只能从s[1]上去 没有其他方法)。
那么就可以推出s[3] ~ s[n]了。
下面继续模拟一下, s[3] = s[1] + s[2], 因为只能从第一级跨两步, 或者第二级跨一步。
function cStairs(n) {
if(n === 1 || n === 2) {
return 1;
} else {
return cStairs(n-1) + cStairs(n-2)
}
}
幂函数
function test(target, count) {
return count === 0 ? 1 : target * test(target, count - 1)
}
console.log(test(-2, 3))
求和组合
const arr = [2, 3, 4, 7, 5, 8, 1, 9, 0]
function sum(arr, target) {
const res = [], temp = []
for (let i = 0; i < arr.length; i++) {
const dif = target - arr[i]
if (temp[dif] !== undefined) {
res.push([arr[temp[dif]], arr[i]])
}
temp[arr[i]] = i
}
return res
}
console.log(sum(arr, 9)) // [ [ 2, 7 ], [ 4, 5 ], [ 8, 1 ], [ 9, 0 ] ]
全排列
const arr = ['a', 'b', 'c']
function getGroup(arr, index = 0, group = []) {
let middle = []
middle.push(arr[index])
for (let i = 0; i < group.length; i++) {
middle.push(group[i] + arr[index])
}
group.push(...middle)
if (index + 1 >= arr.length) return group
else return getGroup(arr, index + 1, group)
}
console.log(getGroup(arr))
两数相加
(2 -> 4 -> 3) + (5 ->6 -> 4) = (7 -> 0 -> 8)
原因:342 + 456 = 807
function ListNode(val) {
this.val = val
this.next = null
}
function addTwo(num1, num2) {
let node = new ListNode(null)
let cloneNode = node
let current = 0
let next = 0
const createList = (num) => {
let node = new ListNode(null)
let cloneNode = node
String(num).split('').reverse().map(item => {
cloneNode.next = new ListNode(Number(item))
cloneNode = cloneNode.next
})
return node.next
}
num1 = createList(num1)
num2 = createList(num2)
while (num1 !== null || num2 !== null) {
let x = (num1 !== null) ? num1.val : 0
let y = (num2 !== null) ? num2.val : 0
current = (x + y + next) % 10
next = Math.floor((x + y + next) / 10)
cloneNode.next = new ListNode(current)
cloneNode = cloneNode.next
num1 = num1 && num1.next
num2 = num2 && num2.next
}
if (next) { // 解决尾巴进制
cloneNode.next = new ListNode(next)
}
return node.next
}
console.log(addTwo(342, 465)) // 7 -> 0 -> 8
最长不重复字符串
let str = 'asdasdss'
function lengthOfLongestString(str) {
let arr = []
let max = 0
for (let i of str) {
if (arr.includes(i)) {
let index = arr.indexOf(i)
arr.splice(0, index + 1)
}
arr.push(i)
max = max > arr.length ? max : arr.length
}
return max
}
console.log(lengthOfLongestString(str)) // 3
获取最大回文字符串
const str = 'asdfsdgsdfaafd'
function logestPalindrome(str) {
if (!str || str.length < 2) return str
let start = 0, end = 0;
let n = str.length
// 中心扩散法
const centerExpend = (left, right) => {
while (left >= 0 && right < n && str[left] === str[right]) {
left--
right++
}
return right - left - 1
}
for (let i = 0; i < n; i++) {
let len1 = centerExpend(i, i)
let len2 = centerExpend(i, i + 1)
let maxLen = Math.max(len1, len2)
// 获取位置坐标
if (maxLen > end - start) {
start = i - Math.floor(maxLen / 2 - 1)
end = i + Math.floor(maxLen / 2 + 1)
}
}
return str.substring(start, end + 1)
}
console.log(logestPalindrome(str)) // dfaafd
最大盛水容器
const arr = [1, 8, 6, 2, 5, 4, 8, 3, 7]
function maxArea(arr) {
let i = 0,
j = arr.length - 1,
res = 0,
lastHeight = 0;
while (i < j) {
if (arr[i] < arr[j]) {
if (arr[i] > lastHeight) {
res = Math.max(res, (j - i) * arr[i])
lastHeight = arr[i]
}
i++
} else {
if (arr[j] > lastHeight) {
res = Math.max(res, (j - i) * arr[j])
lastHeight = arr[j]
}
j--
}
}
return res
}
console.log(maxArea(arr)) // 49