跟chatGPT一起复习前端 —— 数据结构与算法

135 阅读26分钟

前言

又到一波面试准备的时刻,整理了一波前端相关要点内容,配合chatGPT完成要点内容整理,有纠正错误和需要补充的小伙伴可以在这里留言,及时更新。

链表

构建链表

你可以使用 JavaScript 中的对象来构建链表。你可以使用一个对象来表示一个节点,这个节点包含两个属性:value 和 next。其中,value 属性用于存储节点的值,next 属性用于存储下一个节点的引用。你可以使用一个对象来表示链表,这个对象包含两个属性:head 和 tail。其中,head 属性用于存储链表的头节点,tail 属性用于存储链表的尾节点。你可以使用 JavaScript 中的数组或字符串来创建链表。

以下是 JavaScript 代码实现构建链表的示例代码:

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class LinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
  }

  append(value) {
    const node = new Node(value);

    if (!this.head) {
      this.head = node;
      this.tail = node;

      return this;
    }

    this.tail.next = node;
    this.tail = node;

    return this;
  }
}

const list = new LinkedList();

list.append(1);
list.append(2);
list.append(3);

console.log(list);

翻转链表

function reverseList(head) {
  let prev = null;
  let curr = head;

  while (curr !== null) {
    const next = curr.next;
    curr.next = prev;
    prev = curr;
    curr = next;
  }

  return prev;
}

这个函数接受一个链表的头节点作为参数,并返回反转后的链表的头节点。它使用了一个while循环来遍历链表,将每个节点的next指针指向前一个节点,从而实现了链表的反转。

链表排序

你可以使用 JavaScript 实现链表排序。有两种方法可以实现链表排序:插入排序和归并排序。插入排序是一种简单的排序算法,它通过将元素插入到已排序的子列表中来构建最终的排序列表。归并排序是一种更复杂的排序算法,它将列表分成两个子列表,然后递归地对每个子列表进行排序,最后将两个子列表合并为一个已排序的列表。

插入排序:

function insertionSort(head) {
  if (!head || !head.next) return head;
  let dummy = new ListNode(0);
  dummy.next = head;
  let lastSorted = head,
    curr = head.next;
  while (curr) {
    if (lastSorted.val <= curr.val) {
      lastSorted = lastSorted.next;
    } else {
      let prev = dummy;
      while (prev.next.val <= curr.val) {
        prev = prev.next;
      }
      lastSorted.next = curr.next;
      curr.next = prev.next;
      prev.next = curr;
    }
    curr = lastSorted.next;
  }
  return dummy.next;
}

归并排序:

function mergeSort(head) {
  if (!head || !head.next) return head;
  let mid = getMid(head);
  let left = mergeSort(mid.next);
  mid.next = null;
  let right = mergeSort(head);
  return merge(left, right);
}

function getMid(head) {
  let slow = head,
    fast = head;
  while (fast.next && fast.next.next) {
    slow = slow.next;
    fast = fast.next.next;
  }
  return slow;
}

function merge(l1, l2) {
  let dummy = new ListNode(0),
    p1 = l1,
    p2 = l2,
    p3 = dummy;
  while (p1 && p2) {
    if (p1.val < p2.val) {
      p3.next = p1;
      p1 = p1.next;
    } else {
      p3.next = p2;
      p2 = p2.next;
    }
    p3 = p3.next;
  }
  if (p1) p3.next = p1;
  if (p2) p3.next = p2;

  return dummy.next;
}

构建双向链表

function DoublyLinkedList() {
  function Node(data) {
    this.prev = null;
    this.item = data;
    this.next = null;
  }
  this.head = null;
  this.tail = null;
  this.length = 0;

  // 在链表尾部添加元素
  this.append = function(data) {
    var newNode = new Node(data);
    if (this.length === 0) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      newNode.prev = this.tail;
      this.tail.next = newNode;
      this.tail = newNode;
    }
    this.length++;
  };

  // 在链表任意位置插入元素
  this.insert = function(position, data) {
    if (position < 0 || position > this.length) return false;

    var newNode = new Node(data);
    if (this.length === 0) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      if (position === 0) {
        newNode.next = this.head;
        this.head.prev = newNode;
        this.head = newNode;
      } else if (position === this.length) {
        newNode.prev = this.tail;
        this.tail.next = newNode;
        this.tail = newNode;
      } else {
        var current = this.head,
          index = 0;

        while (index++ < position) {
          current = current.next;
        }

        current.prev.next = newNode;
        newNode.prev = current.prev;

        newNode.next = current;
        current.prev = newNode;
      }
    }
    this.length++;
    return true;
  };

  // 删除链表中指定位置的元素
  this.removeAt = function(position) {
    if (position < 0 || position >= this.length) return null;

    var current,
      previous,
      index = 0;

    if (position === 0) {
      current = this.head;

      if (this.length === 1) {
        this.head = null;
        this.tail = null;
      } else {
        this.head.next.prev = null;
        this.head = current.next;
      }
    } else if (position === this.length - 1) {
      current = this.tail;

      if (this.length === 1) {
        this.head = null;
        this.tail = null;
      } else {
        current.prev.next = null;
        this.tail = current.prev;
      }
    } else {
      current = this.head;

      while (index++ < position) {
        previous = current;
        current = current.next;
      }

      previous.next = current.next;
      current.next.prev = previous;
    }

    this.length--;
    return current.item;
  };
}

这是一个简单的双向链表实现,包括在链表尾部添加元素、在链表任意位置插入元素和删除链表中指定位置的元素等方法。

输出该链表中倒数第k个结点

/*function ListNode(x){
    this.val = x;
    this.next = null;
}*/
function FindKthToTail(head, k)
{
    if(head == null || k <= 0) return null;
    let p1 = head;
    let p2 = head;
    for(let i = 1; i < k; i++){
        if(p1.next != null){
            p1 = p1.next;
        }else{
            return null;
        }
    }
    while(p1.next != null){
        p1 = p1.next;
        p2 = p2.next;
    }
    return p2;
}

输出环形链表的入口结点

/*function ListNode(x){
    this.val = x;
    this.next = null;
}*/
function EntryNodeOfLoop(pHead)
{
    if(pHead == null || pHead.next == null) return null;
    let p1 = pHead;
    let p2 = pHead;
    while(p2 != null && p2.next != null){
        p1 = p1.next;
        p2 = p2.next.next;
        if(p1 == p2){
            p1 = pHead;
            while(p1 != p2){
                p1 = p1.next;
                p2 = p2.next;
            }
            return p1;
        }
    }
    return null;
}

输入两个链表,找出它们的第一个公共结点。

/*function ListNode(x){
    this.val = x;
    this.next = null;
}*/
function FindFirstCommonNode(pHead1, pHead2)
{
    if(pHead1 == null || pHead2 == null) return null;
    let p1 = pHead1;
    let p2 = pHead2;
    while(p1 != p2){
        p1 = (p1 == null) ? pHead2 : p1.next;
        p2 = (p2 == null) ? pHead1 : p2.next;
    }
    return p1;
}

二叉树

二叉树前序遍历

前序遍历的顺序是先访问根节点,然后遍历左子树,最后遍历右子树。下面是一个JavaScript实现的前序遍历的例子:

function preOrder(node) {
  if (node) {
    console.log(node.value);
    preOrder(node.left);
    preOrder(node.right);
  }
}

其中,node是二叉树的节点,value是节点的值,leftright分别是左右子树。这个函数会先访问当前节点的值,然后递归地访问左子树和右子树。

这个二叉树的前序遍历的时间复杂度是O(n),空间复杂度是O(h)。其中,n是二叉树的节点数,h是二叉树的高度。

二叉树中序遍历

中序遍历的顺序是先遍历左子树,然后访问根节点,最后遍历右子树。下面是一个JavaScript实现的中序遍历的例子:

function inOrder(node) {
  if (node) {
    inOrder(node.left);
    console.log(node.value);
    inOrder(node.right);
  }
}

其中,node是二叉树的节点,value是节点的值,leftright分别是左右子树。这个函数会先递归地访问左子树,然后访问当前节点的值,最后递归地访问右子树。

这个二叉树的中序遍历的时间复杂度是O(n),空间复杂度是O(h)。其中,n是二叉树的节点数,h是二叉树的高度。

二叉树后序遍历

后序遍历的顺序是先遍历左子树,然后遍历右子树,最后访问根节点。下面是一个JavaScript实现的后序遍历的例子:

function postOrder(node) {
  if (node) {
    postOrder(node.left);
    postOrder(node.right);
    console.log(node.value);
  }
}

其中,node是二叉树的节点,value是节点的值,leftright分别是左右子树。这个函数会先递归地访问左子树,然后递归地访问右子树,最后访问当前节点的值。

这个二叉树的后序遍历的时间复杂度是O(n),空间复杂度是O(h)。其中,n是二叉树的节点数,h是二叉树的高度。

二叉树重建

根据前序遍历和中序遍历的结果,重建出一棵二叉树。具体实现过程是:先根据前序遍历的结果创建根节点,然后在中序遍历的结果中找到根节点的位置,从而确定左子树和右子树的前序遍历和中序遍历的结果,递归地重建左子树和右子树,最后返回根节点。

function TreeNode(val) {
  this.val = val;
  this.left = this.right = null;
}

function buildTree(preorder, inorder) {
  if (!preorder.length || !inorder.length) return null;
  let root = new TreeNode(preorder[0]);
  let mid = inorder.indexOf(preorder[0]);
  root.left = buildTree(preorder.slice(1, mid + 1), inorder.slice(0, mid));
  root.right = buildTree(preorder.slice(mid + 1), inorder.slice(mid + 1));
  return root;
}

这个二叉树的构建的时间复杂度是O(n),空间复杂度是O(n)。其中,n是二叉树的节点数。

对称二叉树

对称二叉树是指,如果一棵二叉树的左子树和右子树镜像对称,那么这棵二叉树就是对称的。判断一棵二叉树是否对称,可以采用递归的方法。具体实现过程是:从根节点开始,递归地比较左子树和右子树是否对称,如果左子树和右子树都为空,则返回 true;如果左子树和右子树有一个为空,则返回 false;如果左子树和右子树的值不相等,则返回 false;否则,递归地比较左子树的左节点和右子树的右节点,以及左子树的右节点和右子树的左节点是否对称。

function isSymmetric(root) {
  if (!root) return true;
  function check(left, right) {
    if (!left && !right) return true;
    if (!left || !right) return false;
    if (left.val !== right.val) return false;
    return check(left.left, right.right) && check(left.right, right.left);
  }
  return check(root.left, root.right);
}

这个二叉树的对称性判断的时间复杂度是O(n),空间复杂度是O(n)。其中,n是二叉树的节点数。

二叉树镜像

二叉树的镜像是指将一棵二叉树的左右子树交换,得到一棵新的二叉树。这个操作可以递归地进行,即先交换根节点的左右子树,然后递归交换左子树和右子树的左右子树。

你需要递归地交换二叉树中每个节点的左右子节点。如果节点为叶子节点,则无需交换其左右子节点。

function mirror(root) {
  if (root === null) {
    return null;
  }
  const left = mirror(root.left);
  const right = mirror(root.right);
  root.left = right;
  root.right = left;
  return root;
}

这个二叉树的镜像的时间复杂度是O(n),空间复杂度是O(n)。其中,n是二叉树的节点数。

二叉树最大深度

function maxDepth(root) {
  if (root === null) {
    return 0;
  }
  const left = maxDepth(root.left);
  const right = maxDepth(root.right);
  return Math.max(left, right) + 1;
}

这个函数接收一个二叉树的根节点作为参数,返回一个整数,表示二叉树的最大深度。如果输入的二叉树为空,则返回0。

这个二叉树的最大深度的时间复杂度是O(n),空间复杂度是O(h)。其中,n是二叉树的节点数,h是二叉树的高度。

二叉树最小深度

要计算二叉树的最小深度,可以使用递归或广度优先搜索。以下是一个JavaScript实现的例子:

function minDepth(root) {
  if (root === null) {
    return 0;
  }
  if (root.left === null && root.right === null) {
    return 1;
  }
  let min = Number.MAX_SAFE_INTEGER;
  if (root.left !== null) {
    min = Math.min(minDepth(root.left), min);
  }
  if (root.right !== null) {
    min = Math.min(minDepth(root.right), min);
  }
  return min + 1;
}

这个函数接收一个二叉树的根节点作为参数,返回一个整数,表示二叉树的最小深度。如果输入的二叉树为空,则返回0。

这个二叉树的最小深度的时间复杂度是O(n),空间复杂度是O(h)。其中,n是二叉树的节点数,h是二叉树的高度。

平衡二叉树判断

要判断一个二叉树是否为平衡二叉树,可以使用递归。以下是一个JavaScript实现的例子:

function isBalanced(root) {
  if (root === null) {
    return true;
  }
  const left = depth(root.left);
  const right = depth(root.right);
  if (Math.abs(left - right) > 1) {
    return false;
  }
  return isBalanced(root.left) && isBalanced(root.right);
}

function depth(node) {
  if (node === null) {
    return 0;
  }
  const left = depth(node.left);
  const right = depth(node.right);
  return Math.max(left, right) + 1;
}

这个函数接收一个二叉树的根节点作为参数,返回一个布尔值,表示这个二叉树是否为平衡二叉树。如果输入的二叉树为空,则返回true。

这个二叉树的平衡性判断的时间复杂度是O(nlogn),空间复杂度是O(h)。其中,n是二叉树的节点数,h是二叉树的高度。

二叉树每一个根节点到一个叶子结点算一条路径,算出来路径和

要计算二叉树中所有根节点到叶子节点的路径和,可以使用递归。以下是一个JavaScript实现的例子 :

function sumNumbers(root) {
  return dfs(root, 0);
}

function dfs(node, prevSum) {
  if (node === null) {
    return 0;
  }
  const sum = prevSum * 10 + node.val;
  if (node.left === null && node.right === null) {
    return sum;
  }
  return dfs(node.left, sum) + dfs(node.right, sum);
}

这个函数接收一个二叉树的根节点作为参数,返回一个整数,表示所有根节点到叶子节点的路径和。如果输入的二叉树为空,则返回0。

这个二叉树的数字之和的时间复杂度是O(n),空间复杂度是O(h)。其中,n是二叉树的节点数,h是二叉树的高度。

数组

双指针

双指针法是一种常用的算法,主要运用在数组和链表中。它定义了两个指针在指定的数组/链表上游走,在做一些自定义的操作。双指针有左右指针、快慢指针、滑动窗口三种类型,一般时间复杂度为O(n),空间复杂度为O(1)。

调整数组顺序使奇数位于偶数前面

你可以使用双指针的方法,一个指针从前往后找偶数,一个指针从后往前找奇数,然后交换两个指针所指的元素。这样做的时间复杂度是O(n),空间复杂度是O(1)。

以下是JavaScript实现代码:

function reOrderArray(array)
{
    if(array == null || array.length == 0)
        return;
    var i = 0, j = array.length - 1;
    while(i < j){
        while(i < j && !isEven(array[i]))
            i++;
        while(i < j && isEven(array[j]))
            j--;
        if(i < j){
            var temp = array[i];
            array[i] = array[j];
            array[j] = temp;
        }
    }
}

function isEven(n){
    return (n & 1) == 0;
}

这个数组的奇偶排序的时间复杂度是O(n),空间复杂度是O(1)。其中,n是数组的长度。

在连续正整数序列中找出和为S的两个数

你可以使用双指针的方法,一个指针从前往后找数,一个指针从后往前找数,然后判断两个指针所指的元素之和是否等于目标值。如果等于目标值,则返回这两个数。这样做的时间复杂度是O(n),空间复杂度是O(1)。

以下是JavaScript实现代码:

function findContinuousSequence(sum) {
    let left = 1;
    let right = 2;
    const result = [];
    while (left < right) {
        const currentSum = (left + right) * (right - left + 1) / 2;
        if (currentSum === sum) {
            const sequence = [];
            for (let i = left; i <= right; i++) {
                sequence.push(i);
            }
            result.push(sequence);
            left++;
        } else if (currentSum < sum) {
            right++;
        } else {
            left++;
        }
    }
    return result;
}

你需要先判断目标值是否大于1,如果大于1,再按照上述方法查找连续正整数序列。

这个连续序列的和的时间复杂度是O(n),空间复杂度是O(1)。其中,n是序列的长度。

数组中两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

以下是JavaScript实现代码:

function twoSum(nums, target) {
    const map = new Map();
    for (let i = 0; i < nums.length; i++) {
        const complement = target - nums[i];
        if (map.has(complement)) {
            return [map.get(complement), i];
        }
        map.set(nums[i], i);
    }
}

你需要先将数组和目标值作为参数传入函数,然后调用函数即可。

这个数组的两数之和的时间复杂度是O(n),空间复杂度是O(n)。其中,n是数组的长度。

数组中三数之和

function findThreeNumbersWithSum(array, sum) {
  const result = [];
  array.sort((a, b) => a - b);
  for (let i = 0; i < array.length - 2; i++) {
    if (i > 0 && array[i] === array[i - 1]) {
      continue;
    }
    let left = i + 1;
    let right = array.length - 1;
    while (left < right) {
      const currentSum = array[i] + array[left] + array[right];
      if (currentSum === sum) {
        result.push([array[i], array[left], array[right]]);
        while (left < right && array[left] === array[left + 1]) {
          left++;
        }
        while (left < right && array[right] === array[right - 1]) {
          right--;
        }
        left++;
        right--;
      } else if (currentSum < sum) {
        left++;
      } else {
        right--;
      }
    }
  }
  return result;
}

这个函数接收一个数组和一个整数作为参数,返回一个包含三个数字的数组,这三个数字的和等于输入的整数。如果输入的数组中没有这样的三个数字,则返回一个空数组。

这个数组的三数之和的时间复杂度是O(n^2),空间复杂度是O(1)。其中,n是数组的长度。

二维数组

构建乘积数组

构建乘积数组是一道经典的算法题,它的目标是给定一个数组A,要求返回数组B,数组B每个元素等于数组A所有元素除了对应下标以外的全部元素的乘积。这个问题可以通过两个循环来解决,时间复杂度为O(n)。

当给定数组A为[1, 2, 3, 4, 5]时,数组B应该为[120, 60, 40, 30, 24]。你可以参考以下代码实现:

var constructArr = function (a) {
  const result = [];
  let testarr = [];
  let l = 0;
  let r = a.length - 1;
  let temp = 1;
  let temp2 = 1;
  for (let i = 0; i < a.length; i++) {
    testarr.push(...a.slice(0, i), ...a.slice(i + 1));
    result.push(testarr.reduce((pre, cur) => pre * cur, 1));
    testarr = [];
  }
  return result;
};

这个算法的时间复杂度为O(n^2),空间复杂度为O(n)。

数据统计

数组中出现次数超过数组长度一半的数字

这是一道经典的算法题目。这个问题可以通过多种方法解决,例如哈希表、排序、摩尔投票法等等。其中,哈希表是最常用的方法之一。你可以利用对象的特性将数组中的数出现的次数统计出来,然后遍历对象,找到出现次数超过数组长度一半的数字即可。

这里有一个示例代码,你可以参考一下:

function MoreThanHalfNum_Solution(numbers) {
  if (!numbers || numbers.length === 0) return 0;
  let result = numbers[0];
  let times = 1;
  for (let i = 1; i < numbers.length; i++) {
    if (times === 0) {
      result = numbers[i];
      times = 1;
    } else if (numbers[i] === result) {
      times++;
    } else {
      times--;
    }
  }
  let count = 0;
  for (let i = 0; i < numbers.length; i++) {
    if (numbers[i] === result) count++;
  }
  return count > numbers.length / 2 ? result : 0;
}

这个函数的作用是找到数组中出现次数超过数组长度一半的数字。你可以将你的数组作为参数传递给这个函数,然后它会返回这个数字。

这个数组的出现次数超过一半的数字的时间复杂度是O(n),空间复杂度是O(1)。其中,n是数组的长度。

连续子数组的最大和

输入:[-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组 [4,-1,2,1] 的和最大,为 6。

这里是一个JavaScript实现的示例:

function maxSubArray(nums) {
    let maxSum = nums[0];
    let sum = 0;
    for (let i = 0; i < nums.length; i++) {
        sum += nums[i];
        maxSum = Math.max(maxSum, sum);
        if (sum < 0) {
            sum = 0;
        }
    }
    return maxSum;
}

这个数组的最大子序和的时间复杂度是O(n),空间复杂度是O(1)。其中,n是数组的长度。

扑克牌顺子

扑克牌顺子问题是一个经典的算法问题。这个问题的解法有很多种,其中一种是先把数组排序,再统计数组中0的个数,统计排序之后的数组中相邻数字之间的空缺总数,如果空缺的总数小于或者等于0的个数,那么这个数组就是连续的,反之则不连续。时间复杂度为O(n)。

这里是一个JavaScript实现的示例:

function isContinuous(numbers) {
    if (!numbers || numbers.length !== 5) {
        return false;
    }
    numbers.sort((a, b) => a - b);
    let zeroCount = 0;
    let gapCount = 0;
    for (let i = 0; i < numbers.length && numbers[i] === 0; i++) {
        zeroCount++;
    }
    for (let i = zeroCount; i < numbers.length - 1; i++) {
        if (numbers[i + 1] === numbers[i]) {
            return false;
        }
        gapCount += numbers[i + 1] - numbers[i] - 1;
    }
    return gapCount <= zeroCount;
}

这个算法的时间复杂度是O(n),其中n是数组的长度。空间复杂度是O(1),因为我们只需要常数级别的额外空间来存储一些变量。

第一个只出现一次的字符

这个问题有很多种解法。一种解法是使用hash表来查询,即使用HashMap来实现;首先遍历字符串的每一个字符,将字符作为HashMap的key,然后使用Integer作为HashMap的value,当key相同的时候,value就加1。 遍历完之后,再对HashMap做遍历,找出key对应的value=1的key,第一个key就是查找第一个只出现一次的字符。

JavaScript示例如下:

function firstNotRepeatingChar(str) {
  if (!str) return null;
  let map = new Map();
  for (let i = 0; i < str.length; i++) {
    let char = str[i];
    if (map.has(char)) {
      map.set(char, map.get(char) + 1);
    } else {
      map.set(char, 1);
    }
  }
  for (let i = 0; i < str.length; i++) {
    let char = str[i];
    if (map.get(char) === 1) {
      return char;
    }
  }
  return null;
}

这个字符串的第一个只出现一次的字符的时间复杂度是O(n),空间复杂度是O(n)。其中,n是字符串的长度。

还有一种解法是暴力求解O(n^2),由于要求是发现第一个只出现一次的既不重复的字符,采用双层循环结构,内层循环遍历与外层循环遍历逐个比对是否有相同,有则排除继续遍历,直到发现没有重复的。

JavaScript示例如下:

function firstNotRepeatingChar(str) {
  if (!str) return null;
  for (let i = 0; i < str.length; i++) {
    let char = str[i];
    let flag = true;
    for (let j = 0; j < str.length; j++) {
      if (i !== j && char === str[j]) {
        flag = false;
        break;
      }
    }
    if (flag) {
      return char;
    }
  }
  return null;
}

时间复杂度O(n^2),另一方面,它只使用了常量级别的额外空间,因此空间复杂度是O(1)。

还有一种解法是建立一个Hash表,用来存储每一个字符出现的次数,即一个字符的大小:256(无符号类型),对Hash表中的每一个的字符进行初始化。存储数组中每个字符出现的次数。最后再扫描一遍数组,找到第一个出现次数为1的字符。

JavaScript示例如下:

function firstNotRepeatingChar(str) {
  if (!str) return null;
  let hashTable = new Array(256).fill(0);
  for (let i = 0; i < str.length; i++) {
    let char = str[i];
    hashTable[char.charCodeAt()]++;
  }
  for (let i = 0; i < str.length; i++) {
    let char = str[i];
    if (hashTable[char.charCodeAt()] === 1) {
      return char;
    }
  }
  return null;
}

这是一个查找第一个不重复字符的函数。它使用了一个哈希表来存储每个字符出现的次数,然后再遍历一次字符串来找到第一个不重复的字符。如果字符串为空,则返回null。

这个函数的时间复杂度是O(n),空间复杂度是O(1)。它使用了一个哈希表来存储每个字符出现的次数,然后再遍历一次字符串来找到第一个不重复的字符。由于只需要遍历一次字符串,因此时间复杂度是O(n)。另一方面,哈希表的大小是常量级别的,因此空间复杂度是O(1)。

栈和队列

实现一个栈

class Stack {
  constructor() {
    this.items = [];
  }
  push(element) {
    this.items.push(element);
  }
  pop() {
    if (this.items.length === 0) return "Underflow";
    return this.items.pop();
  }
  peek() {
    return this.items[this.items.length - 1];
  }
  isEmpty() {
    return this.items.length === 0;
  }
}

这个实现使用一个数组来存储栈的元素。push()方法将一个元素添加到栈的顶部,而pop()方法则删除并返回顶部元素。peek()方法返回顶部元素而不删除它,而isEmpty()方法则检查栈是否为空。

实现一个队列

队列是一种遵循先入先出(FIFO)规则的数据结构。在 JavaScript 中,你可以使用类来实现队列数据结构。你可以在类中定义一个数组来存储队列元素,然后实现以下方法:enqueue,dequeue,peek 和 length。下面是一个简单的 JavaScript 队列实现的例子。

class Queue {
  constructor() {
    this.elements = [];
    this.head = 0;
    this.tail = 0;
  }

  get length() {
    return this.tail - this.head;
  }

  enqueue(element) {
    this.elements[this.tail++] = element;
  }

  dequeue() {
    if (this.length === 0) return undefined;
    const element = this.elements[this.head];
    delete this.elements[this.head++];
    return element;
  }

  peek() {
    if (this.length === 0) return undefined;
    return this.elements[this.head];
  }
}

你可以使用上面的代码来创建一个队列实例并调用其方法。例如:

const queue = new Queue();
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
console.log(queue.peek()); // 输出:1
console.log(queue.dequeue()); // 输出:1
console.log(queue.dequeue()); // 输出:2
console.log(queue.dequeue()); // 输出:3

队列和栈的互相实现

栈和队列是两种不同的数据结构,它们的实现方式也不同。栈是一种后进先出的数据结构,而队列是一种先进先出的数据结构。它们之间可以相互实现,但需要借助一个辅助的栈或队列。

下面是用队列实现栈的算法的代码:

class Stack {
  constructor() {
    this.queue = [];
  }
  push(x) {
    this.queue.push(x);
    for (let i = 0; i < this.queue.length - 1; i++) {
      this.queue.push(this.queue.shift());
    }
  }
  pop() {
    return this.queue.shift();
  }
  top() {
    return this.queue[0];
  }
  empty() {
    return this.queue.length === 0;
  }
}

下面是用栈实现队列的算法的代码:

class Queue {
  constructor() {
    this.stack1 = [];
    this.stack2 = [];
  }
  push(x) {
    this.stack1.push(x);
  }
  pop() {
    if (this.stack2.length === 0) {
      while (this.stack1.length > 0) {
        this.stack2.push(this.stack1.pop());
      }
    }
    return this.stack2.pop();
  }
  peek() {
    if (this.stack2.length === 0) {
      while (this.stack1.length > 0) {
        this.stack2.push(this.stack1.pop());
      }
    }
    return this.stack2[this.stack2.length - 1];
  }
  empty() {
    return this.stack1.length === 0 && this.stack2.length === 0;
  }
}

包含min函数的栈

你可以使用两个栈来实现,一个栈用来存储数据,另一个栈用来存储当前栈中的最小值。每次插入数据时,如果该数据比当前栈中的最小值还要小,则将该数据同时插入到存储最小值的栈中。每次弹出数据时,如果弹出的数据是当前栈中的最小值,则同时从存储最小值的栈中弹出该最小值。这样就可以在O(1)时间内得到栈中所含最小元素了。

var stack = [];
var minStack = [];
function push(node) {
    stack.push(node);
    if (minStack.length === 0 || node < minStack[minStack.length - 1]) {
        minStack.push(node);
    } else {
        minStack.push(minStack[minStack.length - 1]);
    }
}
function pop() {
    stack.pop();
    minStack.pop();
}
function top() {
    return stack[stack.length - 1];
}
function min() {
    return minStack[minStack.length - 1];
}

滑动窗口最大值

滑动窗口最大值是指给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组 {2, 3, 4, 2, 6, 2, 5, 1} 及滑动窗口的大小 3,那么一共存在 6 个滑动窗口,他们的最大值分别为 {4, 4, 6, 6, 6, 5}。

这个问题可以使用双端队列来解决。具体来说,我们维护一个双端队列,队列中存储的是数组元素的下标。我们遍历数组,如果队列为空,则直接将当前元素的下标加入队列;否则,我们比较当前元素和队列尾部存储的元素所对应的数组元素大小。如果当前元素小于等于队列尾部存储的元素所对应的数组元素,则将当前元素的下标加入队列;否则,我们将队列尾部存储的元素所对应的数组下标从队列中删除,并将当前元素的下标加入队列。此时,如果队列头部存储的元素所对应的数组下标已经不在当前滑动窗口内,则将其从队列中删除。

function maxSlidingWindow(nums, k) {
    if (nums == null || nums.length == 0 || k <= 0) {
        return [];
    }
    let deque = [];
    let res = [];
    for (let i = 0; i < nums.length; i++) {
        while (deque.length > 0 && nums[i] >= nums[deque[deque.length - 1]]) {
            deque.pop();
        }
        deque.push(i);
        if (deque[0] <= i - k) {
            deque.shift();
        }
        if (i >= k - 1) {
            res.push(nums[deque[0]]);
        }
    }
    return res;
}

这是一个滑动窗口的函数,用于在一个数组中找到每个窗口的最大值。如果数组为空或k小于等于0,则返回空数组。

这个函数的时间复杂度是O(n),空间复杂度是O(k)。它使用了一个双端队列来存储每个窗口的最大值。由于每个元素最多只会被加入和弹出一次,因此时间复杂度是O(n)。另一方面,队列的大小最多为k,因此空间复杂度是O(k)。

接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6

JavaScript 实现的例子

var trap = function(height) {
    let left = 0;
    let right = height.length - 1;
    let leftMax = 0;
    let rightMax = 0;
    let res = 0;
    while (left < right) {
        if (height[left] < height[right]) {
            if (height[left] >= leftMax) {
                leftMax = height[left];
            } else {
                res += leftMax - height[left];
            }
            left++;
        } else {
            if (height[right] >= rightMax) {
                rightMax = height[right];
            } else {
                res += rightMax - height[right];
            }
            right--;
        }
    }
    return res;
};

这是一个计算雨水的函数,用于计算一个数组中可以存储的雨水量。如果数组为空,则返回0。

这个函数的时间复杂度是O(n),空间复杂度是O(1)。它使用了两个指针来遍历数组,并使用两个变量来存储左右两侧的最大高度。由于每个元素最多只会被访问一次,因此时间复杂度是O(n)。另一方面,它只使用了常量级别的额外空间,因此空间复杂度是O(1)。

LRU缓存的简单实现

LRU缓存算法是一种常见的缓存淘汰算法,当缓存满时,淘汰最近最久未使用的元素,在很多分布式缓存系统(如 Redis, Memcached)中都有广泛使用。LRU算法实际上是让你设计数据结构:首先要接收一个 capacity 参数作为缓存的最大容量,然后实现两个 API,一个是 put (key, val) 方法存入键值对,另一个是 get (key) 方法获取 key 对应的 val,如果 key 不存在则返回 -1。

class LRUCache {
  constructor(capacity) {
    this.cache = new Map();
    this.capacity = capacity;
  }
  get(key) {
    if (this.cache.has(key)) {
      let temp = this.cache.get(key);
      this.cache.delete(key);
      this.cache.set(key, temp);
      return temp;
    }
    return -1;
  }
  put(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size >= this.capacity) {
      this.cache.delete(this.cache.keys().next().value);
    }
    this.cache.set(key, value);
  }
}

JavaScript堆是一种用于复杂数据类型(引用类型)分配空间的数据结构,例如数组对象、object对象。堆是运行时动态分配内存的,因此存取速度较慢。JavaScript堆不需要程序代码来显示地释放,因为堆是由自动的垃圾回收来负责的,每种浏览器中的JavaScript解释引擎有不同的自动回收方式.

数据流中的中位数

您好!这是一个经典的问题,可以使用两个堆来解决。一个大根堆存储数据流中较小的一半,一个小根堆存储数据流中较大的一半。这样,中位数就是两个堆顶的平均值或者大根堆的堆顶。

以下是一个 JavaScript 实现的例子:

class MedianFinder {
  constructor() {
    this.small = new Heap((a, b) => b - a); // 大根堆
    this.large = new Heap((a, b) => a - b); // 小根堆
  }

  addNum(num) {
    if (this.small.size() >= this.large.size()) {
      this.small.push(num);
      this.large.push(this.small.pop());
    } else {
      this.large.push(num);
      this.small.push(this.large.pop());
    }
  }

  findMedian() {
    if (this.small.size() === this.large.size()) {
      return (this.small.top() + this.large.top()) / 2;
    } else {
      return this.small.top();
    }
  }
}

class Heap {
  constructor(compare) {
    this.data = [];
    this.compare = compare;
  }

  push(val) {
    this.data.push(val);
    this.bubbleUp(this.data.length - 1);
  }

  pop() {
    const top = this.top();
    const last = this.data.pop();
    if (this.data.length > 0) {
      this.data[0] = last;
      this.bubbleDown(0);
    }
    return top;
  }

  top() {
    return this.data[0];
  }

  size() {
    return this.data.length;
  }

  bubbleUp(idx) {
    while (idx > 0) {
      const parentIdx = (idx - 1) >> 1;
      if (this.compare(this.data[idx], this.data[parentIdx]) < 0) break;
      [this.data[idx], this.data[parentIdx]] = [this.data[parentIdx], this.data[idx]];
      idx = parentIdx;
    }
  }

  bubbleDown(idx) {
    while (idx < this.data.length) {
      const leftChildIdx = idx * 2 + 1;
      const rightChildIdx = idx * 2 + 2;
      let maxChildIdx = idx;
      if (leftChildIdx < this.data.length && 
          this.compare(this.data[leftChildIdx], this.data[maxChildIdx]) > 0) {
        maxChildIdx = leftChildIdx;
      }
      if (rightChildIdx < this.data.length && 
          this.compare(this.data[rightChildIdx], this.data[maxChildIdx]) > 0) {
        maxChildIdx = rightChildIdx;
      }
      if (maxChildIdx === idx) break;
      [this.data[idx], this.data[maxChildIdx]] = [this.data[maxChildIdx], this.data[idx]];
      idx = maxChildIdx;
    }
  }
}

这是一个查找中位数的类,它使用了两个堆来存储数据。如果数据流为空,则返回null。

这个类的时间复杂度是O(log n),空间复杂度是O(n)。它使用了两个堆来存储数据,其中一个是大根堆,另一个是小根堆。由于每次添加元素时,最多只会对两个堆进行一次插入和一次弹出操作,因此时间复杂度是O(log n)。另一方面,它使用了两个堆来存储数据,因此空间复杂度是O(n)。

最小的k个数

您可以使用以下方法来求最小的k个数:

排序:将数组排序,然后取前k个数即可。时间复杂度为O(nlogn)。 堆:维护一个大小为k的堆,遍历数组,如果堆的大小小于k,则将当前元素加入堆中;否则,如果当前元素比堆顶元素小,则将堆顶元素替换为当前元素。时间复杂度为O(nlogk)。 快排思想:基于数组的第k个数字来调整,使得比第k个数字小的所有数字都位于数组的左边,比第k个数字大的所有数字都位于数组的右边。这样调整之后,位于数组中左边的k个数字就是最小的k个数字(这k个数字不一定是排序的)。时间复杂度为O(n)。

以上方法均可求解最小的k个数问题。

以下是JavaScript实现最小的k个数的堆方法的代码。这里我们使用了小根堆,即堆顶元素为最小值。代码中,我们使用了ES6的语法,如箭头函数、解构赋值等。代码中,我们使用了数组的slice方法来复制数组,避免对原数组进行修改。代码如下:

function getLeastNumbers(arr, k) {
    if (k === 0) return [];
    const heap = arr.slice(0, k);
    buildHeap(heap);
    for (let i = k; i < arr.length; i++) {
        if (arr[i] < heap[0]) {
            heap[0] = arr[i];
            heapify(heap, 0);
        }
    }
    return heap;
}

function buildHeap(arr) {
    const len = arr.length;
    for (let i = Math.floor(len / 2); i >= 0; i--) {
        heapify(arr, i);
    }
}

function heapify(arr, i) {
    const left = 2 * i + 1;
    const right = 2 * i + 2;
    let smallest = i;
    if (left < arr.length && arr[left] < arr[smallest]) {
        smallest = left;
    }
    if (right < arr.length && arr[right] < arr[smallest]) {
        smallest = right;
    }
    if (smallest !== i) {
        [arr[i], arr[smallest]] = [arr[smallest], arr[i]];
        heapify(arr, smallest);
    }
}

这是一个查找数组中最小的k个数的函数。它使用了一个大小为k的堆来存储最小的k个数。如果k等于0,则返回空数组。

这个函数的时间复杂度是O(n log k),空间复杂度是O(k)。它使用了一个大小为k的堆来存储最小的k个数。由于每次添加元素时,最多只会对堆进行一次插入和一次弹出操作,因此时间复杂度是O(n log k)。另一方面,它只使用了常量级别的额外空间,因此空间复杂度是O(k)。

最大的K个数

以下是JavaScript实现最大的k个数的堆方法的代码。这里我们使用了大根堆,即堆顶元素为最大值。代码中,我们使用了ES6的语法,如箭头函数、解构赋值等。代码中,我们使用了数组的slice方法来复制数组,避免对原数组进行修改。代码如下:

function getMostNumbers(arr, k) {
    if (k === 0) return [];
    const heap = arr.slice(0, k);
    buildHeap(heap);
    for (let i = k; i < arr.length; i++) {
        if (arr[i] > heap[0]) {
            heap[0] = arr[i];
            heapify(heap, 0);
        }
    }
    return heap;
}

function buildHeap(arr) {
    const len = arr.length;
    for (let i = Math.floor(len / 2); i >= 0; i--) {
        heapify(arr, i);
    }
}

function heapify(arr, i) {
    const left = 2 * i + 1;
    const right = 2 * i + 2;
    let largest = i;
    if (left < arr.length && arr[left] > arr[largest]) {
        largest = left;
    }
    if (right < arr.length && arr[right] > arr[largest]) {
        largest = right;
    }
    if (largest !== i) {
        [arr[i], arr[largest]] = [arr[largest], arr[i]];
        heapify(arr, largest);
    }
}

频率出现最高的前 K 个数

这里是一个使用哈希表的 JavaScript 实现:

function topKFrequent(nums, k) {
    const map = new Map();
    nums.forEach(n => map.set(n, map.get(n) + 1 || 1));
    const list = [...map];
    list.sort((a, b) => b[1] - a[1]);
    return list.slice(0, k).map(n => n[0]);
}

这个函数接受两个参数:一个数字数组和一个数字 K,返回出现频率最高的前 K 个元素。

这里是一个使用堆排序的 JavaScript 实现:

function topKFrequent(nums, k) {
    const map = new Map();
    nums.forEach(n => map.set(n, map.get(n) + 1 || 1));
    const heap = [];
    for (const [num, count] of map) {
        heap.push({ num, count });
        if (heap.length > k) {
            heap.sort((a, b) => a.count - b.count);
            heap.shift();
        }
    }
    return heap.map(n => n.num);
}

排序

冒泡排序

冒泡排序是一种简单直观的排序算法,它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。以下是一个 JavaScript 冒泡排序的例子:

function bubbleSort(arr) {
  var len = arr.length;
  for (var i = 0; i < len - 1; i++) {
    for (var j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {        // 相邻元素两两对比
        var temp = arr[j + 1];        // 元素交换
        arr[j + 1] = arr[j];
        arr[j] = temp;
      }
    }
  }
  return arr;
}

由于它使用了两个嵌套的循环来遍历数组,并在每次遍历时比较相邻的元素,因此时间复杂度是O(n2)。另一方面,它只使用了常量级别的额外空间,因此空间复杂度是O(1)。

选择排序

选择排序是一种简单直观的排序算法,它的工作原理是每一次从待排序的数据元素中选出最小(最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以下是一个 JavaScript 选择排序的例子:

function selectionSort(arr) {
  var len = arr.length;
  var minIndex, temp;
  for (var i = 0; i < len - 1; i++) {
    minIndex = i;
    for (var j = i + 1; j < len; j++) {
      if (arr[j] < arr[minIndex]) {     // 寻找最小的数
        minIndex = j;                 // 将最小数的索引保存
      }
    }
    temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
  }
  return arr;
}

它使用了两个嵌套的循环来遍历数组,并在每次遍历时寻找最小的数。如果找到了最小的数,则将其与当前位置上的数交换。

这个函数的时间复杂度是O(n2)。由于它使用了两个嵌套的循环来遍历数组,并在每次遍历时寻找最小的数,因此时间复杂度是O(n2)。另一方面,它只使用了常量级别的额外空间,因此空间复杂度是O(1)。

快速排序

快速排序是一种常用的排序算法,它的基本思想是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。以下是一个 JavaScript 快速排序的例子:

function quickSort(arr) {
  if (arr.length <= 1) { return arr; }
  var pivotIndex = Math.floor(arr.length / 2);
  var pivot = arr.splice(pivotIndex, 1)[0];
  var left = [];
  var right = [];
  for (var i = 0; i < arr.length; i++){
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return quickSort(left).concat([pivot], quickSort(right));
}

它使用了递归的方式来对数组进行排序。在每次递归中,它选择数组中的一个元素作为基准值,并将数组分成两个部分:小于基准值的部分和大于基准值的部分。然后,它对这两个部分分别进行递归排序,并将它们合并起来。

这个函数的时间复杂度是O(n log n)。由于它使用了递归的方式来对数组进行排序,并且每次递归都会将数组分成两个部分,因此时间复杂度是O(n log n)。另一方面,它使用了递归调用栈来存储每次递归调用的状态,因此空间复杂度是O(log n)。

插入排序

插入排序是一种简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。以下是一个 JavaScript 插入排序的例子:

function insertionSort(arr) {
  for (var i = 1; i < arr.length; i++) {
    var current = arr[i];
    var j = i - 1;
    while ((j > -1) && (current < arr[j])) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = current;
  }
  return arr;
}

这是一个插入排序的函数。它使用了一个循环来遍历数组,并在每次遍历时将当前元素插入到已排序的部分中的正确位置。

这个函数的时间复杂度是O(n2)。由于它使用了一个循环来遍历数组,并在每次遍历时将当前元素插入到已排序的部分中的正确位置,因此时间复杂度是O(n2)。另一方面,它只使用了常量级别的额外空间,因此空间复杂度是O(1)。

归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序的基本思想是将待排序的元素序列分成若干个子序列,每个子序列都是有序的,然后再将子序列合并成整体有序序列。以下是一个 JavaScript 归并排序的例子:

function mergeSort(arr) {
  if (arr.length < 2) return arr;
  var middle = Math.floor(arr.length / 2);
  var left = arr.slice(0, middle);
  var right = arr.slice(middle);
  return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
  var result = [];
  while (left.length && right.length) {
    if (left[0] <= right[0]) {
      result.push(left.shift());
    } else {
      result.push(right.shift());
    }
  }
  while (left.length) result.push(left.shift());
  while (right.length) result.push(right.shift());
  return result;
}

它使用了递归的方式来对数组进行排序。在每次递归中,它将数组分成两个部分,并对这两个部分分别进行递归排序。然后,它将这两个已排序的部分合并起来。

这个函数的时间复杂度是O(n log n)。由于它使用了递归的方式来对数组进行排序,并且每次递归都会将数组分成两个部分,因此时间复杂度是O(n log n)。另一方面,它使用了一个大小为n的临时数组来存储已排序的元素,因此空间复杂度是O(n)。

希尔排序

希尔排序(Shell Sort)是插入排序的一种,它是针对直接插入排序算法的改进。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。希尔排序时间复杂度是 O(n^(1.3-2)) ,空间复杂度为常数阶 O(1)。以下是一个 JavaScript 希尔排序的例子:

function shellSort(arr) {
    var len = arr.length,
        temp,
        gap = 1;
    while (gap < len / 5) { //动态定义间隔序列
        gap = gap * 5 + 1;
    }
    for (gap; gap > 0; gap = Math.floor(gap / 5)) {
        for (var i = gap; i < len; i++) {
            temp = arr[i];
            for (var j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
                arr[j + gap] = arr[j];
            }
            arr[j + gap] = temp;
        }
    }
    return arr;
}

这个函数的时间复杂度是O(n log n)。由于它使用了一个动态定义的间隔序列来对数组进行排序,并且在每次排序中都使用了插入排序,因此时间复杂度是O(n log n)。另一方面,它只使用了常量级别的额外空间,因此空间复杂度是O(1)。

堆排序

堆排序是一种利用堆这种数据结构而设计的一种排序算法,它的最坏、最好、平均时间复杂度均为O(nlogn),是不稳定排序。以下是堆排序的基本思想:

  1. 将待排序序列构造成一个大顶堆。
  2. 此时,整个序列的最大值就是堆顶的根节点。将它与末尾元素进行交换,此时末尾就为最大值。
  3. 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。反复执行此操作,直到整个序列有序。

以下是JavaScript实现堆排序的代码:

function heapSort(arr) {
  let len = arr.length;
  // 初始化大顶堆,从第一个非叶子结点开始
  for (let i = Math.floor(len / 2) - 1; i >= 0; i--) {
    heapify(arr, len, i);
  }
  // 排序,每次将堆顶元素与末尾元素交换,然后重新调整大顶堆
  for (let i = len - 1; i > 0; i--) {
    swap(arr, 0, i);
    heapify(arr, i, 0);
  }
  return arr;
}

function heapify(arr, len, i) {
  let left = 2 * i + 1,
    right = 2 * i + 2,
    largest = i;
  if (left < len && arr[left] > arr[largest]) {
    largest = left;
  }
  if (right < len && arr[right] > arr[largest]) {
    largest = right;
  }
  if (largest !== i) {
    swap(arr, i, largest);
    heapify(arr, len, largest);
  }
}

function swap(arr, i, j) {
  let temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

由于它使用了一个大顶堆来对数组进行排序,并且在每次排序时都需要重新调整大顶堆,因此时间复杂度是O(n log n)。另一方面,它只使用了常量级别的额外空间,因此空间复杂度是O(1)。

二分查找

二分查找是一种在一个有序数组中查找特定元素位置的查找算法。二分查找要求查找序列采用顺序存储,且按关键字有序排列。

旋转数组的最小数字

旋转数组的最小数字是指在一个非递减排序的数组的一个旋转中,找到旋转后的数组的最小值。在JavaScript中,可以使用以下方法进行旋转数组的最小数字查找:①二分法查找,时间复杂度为O(logn);②遍历查找,时间复杂度为O(n)。

以下是通过二分查找实现的JavaScript代码,时间复杂度为O(logn)。

function minNumberInRotateArray(rotateArray)
{
    if (rotateArray.length === 0) {
        return 0;
    }
    let left = 0;
    let right = rotateArray.length - 1;
    while (left < right) {
        let mid = Math.floor((left + right) / 2);
        if (rotateArray[mid] > rotateArray[right]) {
            left = mid + 1;
        } else if (rotateArray[mid] === rotateArray[right]) {
            right--;
        } else {
            right = mid;
        }
    }
    return rotateArray[left];
}

在排序数组中查找数字

在排序数组中查找数字,可以使用二分法查找,时间复杂度为O(logn)。以下是通过二分法实现的JavaScript代码:

function binarySearch(nums, target) {
    let left = 0;
    let right = nums.length - 1;
    while (left <= right) {
        let mid = Math.floor((left + right) / 2);
        if (nums[mid] === target) {
            return mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;
}

如果要查找排序数组中的重复数字,可以使用以下方法:①遍历查找,时间复杂度为O(n);②二分法查找,时间复杂度为O(logn)。

x 的平方根

要在JavaScript中实现x的平方根,可以使用以下方法:①使用Math.sqrt()函数,返回一个数的平方根;②使用二分法查找,时间复杂度为O(logn)。

以下是通过二分法实现的JavaScript代码:

function mySqrt(x) {
    if (x < 2) {
        return x;
    }
    let left = 1;
    let right = Math.floor(x / 2);
    while (left <= right) {
        let mid = Math.floor((left + right) / 2);
        let num = mid * mid;
        if (num === x) {
            return mid;
        } else if (num > x) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return right;
}

递归

斐波拉契数列

斐波那契数列是指这样一个数列:0、1、1、2、3、5、8、13、21、34……在数学上,斐波那契数列以如下被以递推的方法定义:

F(0) = 0 F(1) = 1 F(n) = F(n-1) + F(n-2) (n>=2)

以下是JavaScript实现斐波那契数列的代码:

function fibonacci(n) {
  if (n === 0 || n === 1) {
    return n;
  }
  return fibonacci(n - 1) + fibonacci(n - 2);
}

递归实现斐波那契数列的优点是代码简洁易懂,但当数字太大时,会变得特别慢,原因是在计算F(9)时需要计算F(8)和F(7),而在计算F(8)时需要计算F(7)和F(6),以此类推,导致重复计算1。循环实现斐波那契数列的优点是速度快,不会出现栈溢出的问题,但代码相对较长2。还有一种方法是使用动态规划来实现斐波那契数列,这种方法的优点是速度快,不会出现栈溢出的问题,而且代码简洁易懂。

这个函数的时间复杂度是O(2n)。由于它使用了递归的方式来计算斐波那契数列的第n项,因此时间复杂度是O(2n)。另一方面,它只使用了常量级别的额外空间,因此空间复杂度是O(1)。

以下是一种使用动态规划来实现斐波那契数列的JavaScript代码:

function fibonacci(n) {
    let fib = [0, 1];
    for (let i = 2; i <= n; i++) {
        fib[i] = fib[i - 1] + fib[i - 2];
    }
    return fib[n];
}

这个代码使用了一个数组来存储之前计算过的斐波那契数,避免了重复计算,从而提高了速度。

这个函数的时间复杂度是O(n)。由于它使用了一个循环来计算斐波那契数列的前n项,并且在每次循环中只进行了常量级别的操作,因此时间复杂度是O(n)。另一方面,它使用了一个大小为n的数组来存储斐波那契数列的前n项,因此空间复杂度是O(n)。

跳台阶

跳台阶问题是指一个人可以一次跳1级台阶,也可以一次跳2级台阶。求该人从一个n级台阶跳下来共有多少种跳法。

以下是JavaScript实现跳台阶问题的代码:

function jumpFloor(number) {
  if (number === 1) {
    return 1;
  }
  if (number === 2) {
    return 2;
  }
  let pre1 = 1,
    pre2 = 2,
    result = 0;
  for (let i = 3; i <= number; i++) {
    result = pre1 + pre2;
    pre1 = pre2;
    pre2 = result;
  }
  return result;
}

以下是递归方式实现跳台阶问题的代码:

function jumpFloor(number) {
  if (number === 1) {
    return 1;
  }
  if (number === 2) {
    return 2;
  }
  return jumpFloor(number - 1) + jumpFloor(number - 2);
}

矩形覆盖

矩形覆盖问题是指用2x1的小矩形覆盖2xn的大矩形,问有多少种不同的覆盖方法。

以下是JavaScript实现矩形覆盖问题的代码:

function rectCover(number) {
  if (number === 0) {
    return 0;
  }
  if (number === 1) {
    return 1;
  }
  if (number === 2) {
    return 2;
  }
  let pre1 = 1,
    pre2 = 2,
    result = 0;
  for (let i = 3; i <= number; i++) {
    result = pre1 + pre2;
    pre1 = pre2;
    pre2 = result;
  }
  return result;
}

以下是递归方式实现矩形覆盖问题的代码:

function rectCover(number) {
  if (number === 0) {
    return 0;
  }
  if (number === 1) {
    return 1;
  }
  if (number === 2) {
    return 2;
  }
  return rectCover(number - 1) + rectCover(number - 2);
}

广度优先

广度优先算法(BFS)是一种图形搜索算法,也是最简便的图的搜索算法之一。它的核心思想是从初始节点开始,应用算法生成第一层节点,检查目标节点是否在这些后继节点中,若没有,再用产生式规则将所有第一层的节点逐一扩展,得到第二层节点,并逐一检查第二层节点中是否包含目标节点2。广度优先搜索算法是很多重要的图的算法的原型,如Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想2。

从上到下打印二叉树

这是一道经典的二叉树遍历问题。可以使用层次遍历方法,利用bfs广度优先遍历的方法进行遍历,然后保存到list中,广度优先遍历借助队列结构进行遍历。首先定义res = [] ,queue = [root],如果队列为空则退出遍历,然后节点出队node = queue.pop (),再将node.val加入res中,最后再将node的左右子节点入队 (若左右子节点存在)。

以下是JavaScript实现的代码:

function PrintFromTopToBottom(root) {
    if (!root) return [];
    let res = [], queue = [root];
    while (queue.length) {
        let node = queue.shift();
        res.push(node.val);
        if (node.left) queue.push(node.left);
        if (node.right) queue.push(node.right);
    }
    return res;
}

员工的重要性

员工的重要性算法是指在一个公司中,每个员工都有一个重要度,同时每个员工还有一些直系下属。定义一个函数,输入是所有员工的信息,以及一个员工id,返回这个员工和他所有下属的重要度之和。

这道题目可以使用广度优先搜索或深度优先搜索来解决。如果使用广度优先搜索,我们需要建立一个哈希表来存储每个员工的信息,然后从给定的员工id开始遍历整个哈希表。如果使用深度优先搜索,我们需要建立一个哈希表来存储每个员工的信息,然后从给定的员工id开始遍历整个哈希表。

广度优先代码如下:

var GetImportance = function(employees, id) {
    let map = new Map();
    for (let employee of employees) {
        map.set(employee.id, employee);
    }
    let queue = [];
    queue.push(id);
    let importance = 0;
    while (queue.length > 0) {
        let currentId = queue.shift();
        let currentEmployee = map.get(currentId);
        importance += currentEmployee.importance;
        for (let subordinate of currentEmployee.subordinates) {
            queue.push(subordinate);
        }
    }
    return importance;
};

深度优先搜索

深度优先搜索(DFS)是一种在图中遍历或搜索的算法。它从图中的一个顶点出发,每次遍历当前访问顶点的临界点,一直到访问的顶点没有未被访问过的临界点为止。这种算法不会根据图的结构等信息调整执行策略。深度优先搜索是图论中的经典算法,利用深度优先搜索算法可以产生目标图的拓扑排序表,利用拓扑排序表可以方便地解决很多相关的图论问题,如无权最长路径问题等等。

路径总和

路径总和算法是指给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。这个算法可以使用递归或迭代的方式实现。

这是一个使用递归方式实现路径总和算法的 JavaScript 代码示例:

function hasPathSum(root, targetSum) {
    if (!root) return false;
    if (!root.left && !root.right) return targetSum === root.val;
    return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);
}

岛屿数量

岛屿数量算法是计算一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。

这个问题可以使用 DFS 或 BFS 算法来解决。DFS 算法的主要思路是每次走到一个是 1 的格子,就搜索整个岛屿。网格可以看成是一个无向图的结构,每个格子和它上下左右的四个格子相邻。如果四个相邻的格子坐标合法,且是陆地,就可以继续搜索。在深度优先搜索的时候要注意避免重复遍历。

BFS 算法的主要思路是对每一个元素做 BFS 遍历,遍历是为了置 0。

回溯算法

回溯算法是一种系统地搜索问题的解的方法,也叫试探法。它是一种选优搜索法,按选优条件向前搜索,以达到目标。当探索到某一步时,发现原先选择并不优或达不到目标,就退回到上一步,重新选择,这种走不通就退回再走的技术为回溯法。

回溯算法实际上是一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解。当发现已不满足求解条件时,就“回溯”返回,尝试其他路径。

回溯算法和深度优先搜索(DFS)有很多相似之处,但也有区别。回溯算法在求解过程中需要进行剪枝操作,而DFS则不需要。

和为sum的n个数

var combinationSum = function(candidates, target) {
    const res = []
    const dfs = (start, path, sum) => {
        if (sum >= target) {
            if (sum === target) {
                res.push([...path])
            }
            return
        }
        for (let i = start; i < candidates.length; i++) {
            path.push(candidates[i])
            dfs(i, path, sum + candidates[i])
            path.pop()
        }
    }
    dfs(0, [], 0)
    return res
};

矩阵中的路径

矩阵中的路径算法是一个经典的回溯算法问题,它的目标是在一个矩阵中寻找一条由给定字符串中的字符构成的路径。以下是一个JavaScript实现的矩阵中的路径算法的例子:

var exist = function(board, word) {
    const m = board.length
    const n = board[0].length
    const used = new Array(m).fill(0).map(() => new Array(n).fill(false))
    const dfs = (i, j, k) => {
        if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] !== word[k] || used[i][j]) {
            return false
        }
        if (k === word.length - 1) {
            return true
        }
        used[i][j] = true
        const res = dfs(i + 1, j, k + 1) || dfs(i - 1, j, k + 1) || dfs(i, j + 1, k + 1) || dfs(i, j - 1, k + 1)
        used[i][j] = false
        return res
    }
    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (dfs(i, j, 0)) {
                return true
            }
        }
    }
    return false
};

N皇后问题

N皇后问题是一个经典的问题,研究的是如何将n个皇后放置在n×n的棋盘上,并且使皇后彼此之间不能相互攻击。N皇后问题的两种主要算法是试探回溯法和位运算法。

这里是一个使用回溯算法解决N皇后问题的JavaScript实现 。这个实现使用了一个一维数组来存储每个皇后的位置,数组的下标表示行,数组的值表示列。在每一行中,它会尝试放置皇后,并检查是否与之前的皇后冲突。如果没有冲突,则递归到下一行。如果所有行都被检查过了,则找到了一个解。如果没有找到解,则回溯并尝试下一个位置。

function solveNQueens(n) {
  const result = [];
  const board = Array.from({ length: n }, () => Array.from({ length: n }, () => '.'));
  const cols = new Set();
  const diag1 = new Set();
  const diag2 = new Set();

  function backtrack(row) {
    if (row === n) {
      result.push(board.map(row => row.join('')));
      return;
    }

    for (let col = 0; col < n; col++) {
      if (cols.has(col) || diag1.has(row + col) || diag2.has(row - col)) {
        continue;
      }

      cols.add(col);
      diag1.add(row + col);
      diag2.add(row - col);
      board[row][col] = 'Q';

      backtrack(row + 1);

      cols.delete(col);
      diag1.delete(row + col);
      diag2.delete(row - col);
      board[row][col] = '.';
    }
  }

  backtrack(0);

  return result;
}

你可以通过调用solveNQueens(n)函数来解决N皇后问题,其中n是棋盘大小。

贪心算法

贪心算法是一种寻找最优解问题的常用方法。这种方法模式一般将求解过程分成若干个步骤,但每个步骤都应用贪心原则,选取当前状态下最好/最优的选择(局部最有利的选择),并以此希望最后堆叠出的结果也是最好/最优的。

装满石头的背包的最大数量实现

现有编号从 0 到 n - 1 的 n 个背包。给你两个下标从 0 开始的整数数组 capacity 和 rocks 。第 i 个背包最大可以装 capacity[i] 块石头,当前已经装了 rocks[i] 块石头。另给你一个整数 additionalRocks ,表示你可以放置的额外石头数量,石头可以往 任意 背包中放置。

请你将额外的石头放入一些背包中,并返回放置后装满石头的背包的 最大 数量。

装满石头的背包的最大数量实现是一道经典的贪心问题。请你将额外的石头放入一些背包中,并返回放置后装满石头的背包的最大数量。这道题目我们主要要使用贪心的思想来进行解题,也就是说我们应该想一下怎么放入石头可以使得装满的背包最多?答案其实是很明显的,只要我们优先将石头装入背包容量剩余最少的背包中,那么我们便可以使用最少的石头来装满一个背包,所以我们可以先对差值数组排序,然后先放满小的,在放满大的。

function maxNumberOfBags(capacity, rocks, additionalRocks) {
    let diff = [];
    for (let i = 0; i < capacity.length; i++) {
        diff.push(capacity[i] - rocks[i]);
    }
    diff.sort((a, b) => a - b);
    let count = 0;
    for (let i = 0; i < diff.length && additionalRocks >= diff[i]; i++) {
        additionalRocks -= diff[i];
        count++;
    }
    return count;
}

动态规划

动态规划算法是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。

最长公共子序列

function lcs(wordX, wordY) {
  var m = wordX.length,
    n = wordY.length,
    l = [],
    i, j, a, b;
  for (i = 0; i <= m; ++i) {
    l[i] = [];
    for (j = 0; j <= n; ++j) {
      l[i][j] = 0;
    }
  }
  for (i = m - 1; i >= 0; --i) {
    for (j = n - 1; j >= 0; --j) {
      if (wordX[i] == wordY[j]) {
        l[i][j] = l[i + 1][j + 1] + 1;
      } else {
        a = l[i + 1][j];
        b = l[i][j + 1];
        l[i][j] = (a > b) ? a : b;
      }
    }
  }
  return l[0][0];
}

青蛙跳台问题

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

这是一道经典的递归问题。每次跳的时候,青蛙可以跳一个台阶,也可以跳两个台阶,也就是每次跳的时候,小青蛙有两种跳法。第一次跳的时候,小青蛙有两种选择:跳一个台阶或者跳两个台阶。如果选择跳一个台阶,那么还剩下n-1个台阶还没跳,剩下的n-1个台阶的跳法有f(n-1)种;如果选择跳两个台阶,那么还剩下n-2个台阶还没跳,剩下的n-2个台阶的跳法有f(n-2)种。所以n个台阶的不同跳法总数f(n)=f(n-1)+f(n-2)。

function jumpFloor(number) {
    if (number === 1) {
        return 1;
    } else if (number === 2) {
        return 2;
    } else {
        return jumpFloor(number - 1) + jumpFloor(number - 2);
    }
}

最长递增子序列

最长递增子序列(Longest Increasing Subsequence,LIS)是一个经典的算法问题。它指的是找到一个特定的最长的子序列,并且子序列中的所有元素单调递增。例如, { 3,5,7,1,2,8 } 的 LIS 是 { 3,5,7,8 },长度为 4。这个问题可以使用动态规划法来解决。假设长度为n的数组 A=\ {a_0,a_1,…,a_n} , 以 a_i 元素结尾的最长递增子序列为 L_i , 则当 j<i<n 且 a_i > a_j , L_i= max (L_j +1, L_i) , L_i 的初始值为1。

function longestIncreasingSubsequence(nums) {
    let dp = new Array(nums.length).fill(1);
    let max = 1;
    for (let i = 1; i < nums.length; i++) {
        for (let j = 0; j < i; j++) {
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
        max = Math.max(max, dp[i]);
    }
    return max;
}

零钱兑换

零钱兑换问题是一种经典的动态规划问题。这个问题的目标是找到最少的硬币数量,以便凑出给定的金额。这个问题可以使用动态规划算法来解决。动态规划算法的基本思想是将问题分解成子问题,并使用已知的解决方案来解决它们。

var coinChange = function (coins, amount) {
  // 用无穷大填充数组的每一个元素
  let dp = new Array(amount + 1).fill(Infinity)
  dp[0] = 0
  console.log(dp);
  let len = coins.length;

  for (let i = 0; i <= amount; i++) {
    for (let j = 0; j < len; j++) {
    // 注意:这里的条件左边是钱的价格肯定要比硬币的价额大才可以放进去的
    // 条件的右边是如果数组元素还是初始值肯定就是没有符合条件的硬币放
      if (i >= coins[j] && dp[i - coins[j]] !== Infinity) {
        dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1)
      }
    }
  }
  if (dp[amount] === Infinity) {
    return -1;
  }
  return dp[amount];
};


console.log(coinChange([1, 2, 5], 11));

买卖股票的最佳时

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

这是一道经典的动态规划问题。可以使用动态规划来解决。假设第i天的最大利润为profit[i],则有以下状态转移方程:

profit[i] = max(profit[i-1], prices[i]-minPrice)
minPrice = min(minPrice, prices[i])

其中,minPrice表示前i天的最低股票价格。

你可以使用这个状态转移方程来解决这个问题。

function maxProfit(prices) {
    let minPrice = prices[0];
    let maxProfit = 0;
    for (let i = 1; i < prices.length; i++) {
        maxProfit = Math.max(maxProfit, prices[i] - minPrice);
        minPrice = Math.min(minPrice, prices[i]);
    }
    return maxProfit;
}

贪心算法和动态规划的区别

贪心算法和动态规划都是算法设计中的一种方法,它们都是通过将原问题分解为若干个子问题来求解,但它们的思想和实现方法却不同。

贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。贪心算法通常以自上而下的方法进行,用于解决优化问题。

动态规划算法则通常自下而上的方式解各子问题,以便于后面的子问题可以利用前面子问题的解。动态规划算法本质是穷举法,可以保证结果是最佳的,复杂度高。

因此,贪心算法和动态规划算法之间的区别在于:

  • 贪心算法遵循自上而下的方法;动态规划算法遵循自下而上的方法。
  • 贪心算法用于解决优化问题;动态规划算法用于解决优化问题。
  • 贪心算法每一步的最优解一定包含上一步的最优解,上一步之前的最优解则不作保留;动态规划全局最优解中不一定包含前一个局部最优解,因此需要记录之前的所有的局部最优解。

时间复杂度

时间复杂度是指算法运行所需要的时间,通常用大O表示法来表示。大O表示法是一种用于描述算法复杂度的数学符号,它描述了算法运行时间与输入数据规模之间的关系。例如,如果一个算法的时间复杂度为O(n),那么当输入数据规模为n时,该算法的运行时间为n个单位时间 。

计算时间复杂度的方法有很多种,但是最常用的方法是找出算法的基本操作,然后根据相应的各语句确定它的执行次数,再找出T(n)的同数量级。T(n)是算法执行的时间,同数量级是指当n趋近于无穷大时,两个函数之间的比值趋近于一个常数。例如,如果一个算法的基本操作执行了n次,那么它的时间复杂度就是O(n)。如果一个算法的基本操作执行了n2次,那么它的时间复杂度就是O(n2)。

空间复杂度

空间复杂度是指算法运行所需要的内存空间,通常用大O表示法来表示。大O表示法是一种用于描述算法复杂度的数学符号,它描述了算法运行所需要的内存空间与输入数据规模之间的关系。例如,如果一个算法的空间复杂度为O(n),那么当输入数据规模为n时,该算法所需要的内存空间为n个单位空间。

计算空间复杂度的方法是找出算法的基本操作,然后根据相应的各语句确定它所占用的存储空间,再找出S(n)的同数量级。S(n)是算法所占用的存储空间,同数量级是指当n趋近于无穷大时,两个函数之间的比值趋近于一个常数。例如,如果一个算法需要开辟n个一维数组,则它的空间复杂度就是O(n)。如果一个算法需要开辟n2个一维数组,则它的空间复杂度就是O(n2)。