时间复杂度空间复杂度
时间复杂度对应代码的运行时间
- 常数型复杂度:O(1):没有循环和递归,不管代码多长,都是o(1)
- 对数型复杂度:O(logn): 有递归
- 线性型复杂度:O(n):有一层循环
- 线性对数型复杂度:O(nlogn):有一层循环,并且循环里面还有递归
- k次型复杂度:O(nᵏ):有多少层循环k就是多少
- 指数型复杂度:O(kⁿ)
- 阶乘型复杂度:O(n!)
空间复杂度对应代码运行需要占用的内存
O(1):不管代码多长,没有开辟新的内存空间,就是O(1)
O(n):在循环里面定义了新的变量,开辟了新的内存空间
O(n2):在二维数组的循环里面定义了新的变量,开辟了新的内存空间
基本算法
递归
排序
二分查找
搜索
位运算
哈希算法
贪心算法
分治算法
回溯算法
动态规划
字符串匹配算法
冒泡排序
- 比较相邻的两个元素。如果第一个比第二个大,则交换位置;
- 对每一对相邻元素重复第一个步骤,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
- j < len - 1 - i是因为最后一个元素就不用比较了,并且比较过了也不用参与比较了
const arr = [3, 44, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
// 外层循环控制循环次数
// 内层循环控制两两比较
function bubbleSort(arr: number[]) {
const len = arr.length;
for (let i = 0; i < len; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换位置
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
时间复杂度:O(n^2)
快速排序
- 找一个基准点,一般是数组的第一个元素
- 准备两个空数组 left和right
- 遍历arr,把小于基准点的放left,大于等于基准点的放right
- 递归上面的步骤left和right,并用conact连接left current right
function quickSort(arr: number[]) {
if (arr.length <= 1) {
return arr;
}
const current = arr.splice(0, 1)[0]; // 选取一个基准点
const left = [];
const right = [];
const len = arr.length;
for (let i = 0; i < len; i++) {
if (arr[i] < current) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat(current, quickSort(right));
}
const arr = [3, 44, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
时间复杂度:O(nlogn)
插入排序
- 分为已排序数据和未排序数据
- 第一个元素认为是已排序数据
- 从第二个元素开始,取出它,去和它的前一个元素比较,如果它比前一个小。那么就把前一个往后移一个位置,它就再去和前前一位比较,如果还是比前前一个小,前前一个又往后移一个位置,依次类推。如果它比前一个大,那么位置就不动,
function insertSort(arr: number[]) {
const len = arr.length;
for (let i = 1; i < len; i++) {
const current = arr[i];
let j = i - 1;
while (j >= 0 && arr[j] > current) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = current;
}
}
const arr = [3, 44, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
时间复杂度O(n^2)
希尔排序
将数组分成多个子数组,分别进行插入排序。初始时,选取一个递减的间隔值 gap
(通常为数组长度的一半),然后按照这个间隔将数组分成若干组。然后对每组分别进行插入排序,不断缩小间隔值,直到间隔值为 1,完成最后一次插入排序。
function shellSort(arr) {
const n = arr.length;
for (let gap = Math.floor(n / 2); gap > 0; gap = Math.floor(gap / 2)) {
for (let i = gap; i < n; i++) {
let temp = arr[i];
let j = i;
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
return arr;
}
时间复杂度:O(nlogn)
选择排序
- 双层循环,外层控制依次拿出基准最小值
- 内层控制与最小基准值依次比较,如果比最小基准值小,最小基准值置为该值
- 然后交换最后得到的最小基准值和外层i值的顺序
- 这样每次都能把最小的依次放到前面去
- i + 1代表从内层循环从还未排序过的数据开始循环
function selectionSort(arr: number[]) {
const len = arr.length;
for (let i = 0; i < len; i++) {
let minIndex = i;
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
}
return arr;
}
const arr = [3, 44, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
时间复杂度O(n^2)
归并排序
- 把数组不断分成两份,然后合并
- 合并时,一直比较left和right的第一项,把更小的那项push进数组
- 一直分两份,一直合并,最终就形成小的依次都在左边,大的依次都在右边
function merge(left: number[], right: number[]) {
const result = [];
while (left.length && right.length) {
if (left[0] < right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
return result.concat(left, right);
}
function mergeSort(arr: number[]) {
const len = arr.length;
if (len < 2) {
return arr;
}
const middle = Math.floor(len / 2);
const left = arr.slice(0, middle);
const right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
const arr = [3, 44, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
时间复杂度O(nlogn)
计数排序
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
function countingSort(arr) {
let len = arr.length, b = [], c = [], min = max = arr[0]
for(let i=0; i<len; i++) {
min = min <= arr[i] ? min : arr[i]
max = max >= arr[i] ? max : arr[i]
c[arr[i]] = c[arr[i]] ? c[arr[i]] + 1 : 1 // 计数
}
for(let i=min; i< max; i++) {
c[i+1] = (c[i+1] || 0) + (c[i] || 0)
}
for(let i=len-1; i>=0; i--) {
b[c[arr[i]] - 1] = arr[i]
c[arr[i]]--
}
return b
}
console.log(countingSort([2,3,8,7,1,2,2,2,7,3,9,8,2,1,4]))
// [ 1, 1, 2, 2, 2, 2, 2, 3, 3, 4, 7, 7, 8, 8, 9]
时间复杂度:O(n+k) ,k表示输入的元素是n 个0到k之间的整数
冒泡排序 ≈ 选择排序 ≈ 插入排序< 希尔排序< 堆排序 < 归并排序< 快速排序
in-place:只占用常数内存,不占用额外内存
out-place: 占用额外内存
稳定:相同的数,排序后没有交换位置
不稳定:相同的数,排序后交换了位置
二分查找
var search = function (nums, target) {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
const mid = Math.floor(left + (right - left) / 2);
if (nums[mid] === target) {
return mid;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
}
}
return -1;
};
两数之和
function twoSum(nums: number[], target: number) {
const len = nums.length;
for (let i = 0; i < len; i++) {
for (let j = i + 1; j < len; j++) {
if (nums[i] + nums[j] === target) {
return [i, j];
}
}
}
}
function twoSum2(nums: number[], target: number) {
const len = nums.length;
const numMap: Record<number, number> = {};
for (let i = 0; i < len; i++) {
const num = arr[i];
const diff = target - num;
if (numMap[diff] !== undefined) {
return [numMap[diff], i];
}
numMap[num] = i;
}
}
广度优先
广度优先遍历(Breadth-First Search, BFS)是一种用于遍历或搜索树或图的算法。这种算法从根节点(或任意节点)开始,访问最靠近根节点的节点。
function breadthFirstSearch(root) {
if (!root) {
return;
}
const queue = [root];
while (queue.length) {
const node = queue.shift();
console.log(node);
node?.children?.forEach((child) => {
queue.push(child);
});
}
}
}
// 示例树结构
const tree = {
value: 'root',
children: [
{
value: 'child1',
children: [
{ value: 'grandchild1', children: [] },
{ value: 'grandchild2', children: [] }
]
},
{
value: 'child2',
children: [
{ value: 'grandchild3', children: [] },
{ value: 'grandchild4', children: [] }
]
}
]
};
// 从根节点开始广度优先遍历
breadthFirstSearch(tree);
深度优先
function depthFirstSearch(root) {
if (!root) {
return;
}
console.log(root.value); // 访问当前节点
root?.children?.forEach((child) => {
depthFirstSearch(child);
});
}
// 示例树结构
const tree = {
value: 'root',
children: [
{
value: 'child1',
children: [
{ value: 'grandchild1', children: [] },
{ value: 'grandchild2', children: [] }
]
},
{
value: 'child2',
children: [
{ value: 'grandchild3', children: [] },
{ value: 'grandchild4', children: [] }
]
}
]
};
// 从根节点开始深度优先遍历
depthFirstSearch(tree);
用栈实现队列
class MyQueue {
constructor() {
this.stack1 = [];
this.stack2 = [];
}
push(element) {
this.stack1.push(element);
}
pop() {
if (this.stack2.length) {
return this.stack2.pop();
}
while (this.stack1.length) {
this.stack2.push(this.stack1.pop());
}
return this.stack2.pop();
}
peek() {
if (this.stack2.length) {
return this.stack2[this.stack2.length - 1];
}
return this.stack1[0];
}
empty() {
return !this.stack1.length && !this.stack2.length;
}
}
判断某个链表是否有环
快慢指针,慢的每次走一步,快的每次走两步,如果有环,快的多走一圈后会追上慢的,追上了就是fast === slow,说明有环,如果快的追不上,fast没有next了就代表不是环
function hasCycle(head) {
if (!head || !head.next) {
return false;
}
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (fast === slow) {
return true
}
}
return false
}