前端算法

58 阅读7分钟

时间复杂度空间复杂度

时间复杂度对应代码的运行时间

  • 常数型复杂度: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之间的整数

image.png 冒泡排序 ≈ 选择排序 ≈ 插入排序< 希尔排序< 堆排序 < 归并排序< 快速排序

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
  }