JS数据结构和算法

一、栈

在这里插入图片描述
在这里插入图片描述
leetcode题目:
在这里插入图片描述
在这里插入图片描述

//方法一:栈
var isValid = function (s) {
    if (s.length % 2 === 1) return false;
    const stack = [];
    for (var i = 0; i < s.length; i++) {
        var c = s[i];
        if (c === '(' || c === '[' || c === '{') {
            stack.push(c);
        } else {
            var top = stack[stack.length - 1];
            if (
                (top === '(' && c === ')') ||
                (top === '[' && c === ']') ||
                (top === '{' && c === '}')
            ) {
                stack.pop();
            }
            else {
                return false;
            }
        }
    }
    return stack.length === 0;
};

二、队列

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
leetcode题目:
在这里插入图片描述

三、链表

在这里插入图片描述
在这里插入图片描述

js中没有链表,但可以用对象object来模拟链表:

const a = {
    val: 'a'
}
const b = {
    val: 'b'
}
const c = {
    val: 'c'
}
const d = {
    val: 'd'
}
a.next = b;
b.next = c;
c.next = d;

// 遍历链表
let p = a;
while (p) {
    console.log(p.val);
    p = p.next;
}

//插入链表
const e = {
    val: 'e'
};
c.next = e;
e.next = d;


// 刪除链表
c.next = d;

总结:给链表添加一个虚拟头结点

const ret = new ListNode(0, head);
return ret;

leecode题目:
在这里插入图片描述

解题思路:

  1. 无法直接获取被删除节点的上个节点。
  2. 将被删除的节点转移到下个节点。

解题步骤:

  1. 将被删除节点的值改为下个节点的值。
  2. 删除下个节点。

本题时间复杂度和空间复杂度均为:O(1)

leecode题目:206. 反转链表
在这里插入图片描述

思路:

  1. 反转两个节点:将n+1的next指向n.
  2. 反转多个节点:双指针遍历链表,重复上述操作。

步骤:

  1. 双指针一前一后遍历链表。
  2. 反转双指针。

时间复杂度:O(n) 空间复杂度:O(1)

var reverseList = function (head) {
    let p1 = null;
    let p2 = head;
    while (p2) {
        let temp = p2.next; //提前保存p2的下一个节点
        p2.next = p1; //反转 
        p1 = p2; //把p2的值给p1,相当于p1往前移动一位
        p2 = temp; //把p2.next赋值给p2,相当于p2往前移动一位
    }
    return p1; //循环结束后,p2指向null,返回p1即可
};

leecode题目:2. 两数相加【较难】
在这里插入图片描述

解题步骤:

  1. 新建一个空链表。
  2. 遍历相加的两个链表,模拟相加操作,将个位数追加到新链表上,将十位数留到下一位去相加。

时间/空间复杂度:O(max(m,n)),其中 m 和 n 分别为两个链表的长度.

var addTwoNumbers = function (l1, l2) {
    const l3 = new ListNode();
    let p1 = l1;
    let p2 = l2;
    let p3 = l3;
    let carry = 0;
    while (p1 || p2) {
        const v1 = p1 ? p1.val : 0;
        const v2 = p2 ? p2.val : 0;
        const v3 = v1 + v2 + carry;
        carry = Math.floor(v3 / 10);
        p3.next = new ListNode(v3 % 10);
        if (p1) p1 = p1.next;
        if (p2) p2 = p2.next;
        p3 = p3.next;
    }
    if (carry) {
        p3.next = new ListNode(carry);
    }
    return l3.next;
};

leecode题目:141. 环形链表
在这里插入图片描述

思路:
用一快一慢两个指针遍历链表,如果指针能够相逢,说明链表有环。

时间复杂度:O(n)
空间复杂度:O(1)

var hasCycle = function (head) {
    let p1 = head;
    let p2 = head;
    while (p1 && p2 && p2.next) {
        p1 = p1.next;
        p2 = p2.next.next;
        if(p1===p2){
            return true;
        }
    }
    return false;
};

leecode 题目:21. 合并两个有序链表
在这里插入图片描述

思路:双指针

  1. 新建一个链表,作为返回的结果。
  2. 用指针遍历两个有序链表,并比较两个链表的当前节点,较小者先接入新链表,并将指针后移一步。
  • 时间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。
  • 空间复杂度:O(1)
var mergeTwoLists = function (l1, l2) {
    let res = new ListNode(0);
    let p = res;
    let p1 = l1;
    let p2 = l2;
    while (p1 && p2) {
        if (p1.val < p2.val) {
            p.next = p1;
            p1 = p1.next;
        } else {
            p.next = p2;
            p2 = p2.next;
        }
        p = p.next;
    }
    if (p1) {
        p.next = p1;
    }
    if (p2) {
        p.next = p2;
    }
    return res.next;
};

四、集合

  • 一种无序且唯一的数据结构。
  • ES6中有集合,名为set。
  • 集合的常用操作:去重、判断某元素是否在集合中、求交集…
// 查看集合
const arr = [1, 1, 2, 2, 2];
const set = new Set(arr)//Set(2) {1, 2}
//查看集合的长度
console.log(set.size);//2

// 去重
const arr = [1, 1, 2, 2, 2];
const arr2 = [...new Set(arr)];
console.log(arr2); //[ 1, 2 ]

// 判断元素是否在集合中
const set = new Set(arr);
const has = set.has(3);
console.log(has); //false

//求交集
const set2 = new Set([2, 3]);
const set3 = new Set([...set].filter(item => set2.has(item)));
console.log(set3); //{ 2 }

ES6中的set:

let mySet = new Set();

mySet.add(1);
mySet.add(5);
mySet.add(5);
mySet.add('some text');
let o = { a: 1, b: 2 };
mySet.add(o);
mySet.add({ a: 1, b: 2 });

const has = mySet.has(o);

mySet.delete(5);

for(let [key, value] of mySet.entries()) console.log(key, value);

const myArr = Array.from(mySet);

const mySet2 = new Set([1,2,3,4]);

const intersection = new Set([...mySet].filter(x => mySet2.has(x)));
const difference = new Set([...mySet].filter(x => !mySet2.has(x)));

leecode题目:
在这里插入图片描述

var intersection = function (nums1, nums2) {
    nums1 = new Set(nums1);
    nums2 = new Set(nums2);
    let res = [...nums1].filter(item=>nums2.has(item));
    return res;
};
//时间复杂度:O(m*n)
//空间复杂度:O(m), m为去重后的数组长度

五、字典

  • 与集合类似,字典也是一种存储唯一值的数据结构,但它是以键值对的形式来存储的。
  • ES6中有字典,名为Map。
  • 字典的常用操作:键值对的增删改查。
const m = new Map();

// 增
m.set('a', 'aa');
m.set('b', 'bb');

//查看字典m:
//Map(2) {"a" => "aa", "b" => "bb"}
//字典的长度
console.log(m.size);//2

// 删
m.delete('b');
// m.clear();

// 改
m.set('a', 'aaa');

//查
m.get('a'); //'aaa'

//判断a是否存在字典中
m.has('a'); //true

leecode题目:
在这里插入图片描述

var intersection = function (nums1, nums2) {
    let map = new Map(),
        arr = [];
    nums1.forEach(n => {
        map.set(n, true);
    });
    nums2.forEach(n => {
        if (map.get(n)) {            
            map.delete(n);
            arr.push(n);
        };
    });
    return arr;
};
//时间复杂度:O(m+n)
//空间复杂度:O(m)

leecode题目:
在这里插入图片描述

var isValid = function (s) {
    if (s.length % 2 === 1) return false;
    const stack = [];
    const map = new Map();
    map.set('(',')');
    map.set('{','}');
    map.set('[',']');
    for (var i = 0; i < s.length; i++) {
        var c = s[i];
        if (map.has(c)) {
            stack.push(c);
        } else {
            var top = stack[stack.length - 1];
            if (map.get(top) === c) {
                stack.pop();
            }
            else {
                return false;
            }
        }
    }
    return stack.length === 0;
};
//时间复杂度:O(n)
//空间复杂度:O(n)

leecode题目:
在这里插入图片描述

思路:

  1. 先找出所有的不包含重复字符的子串;
  2. 找出长度最大那个子串,返回其长度即可。

步骤:

  1. 用双指针维护一个滑动窗口,用来剪切子串。
  2. 不断移动右指针,遇到重复字符,就把左指针移动到重复字符的下一位。
  3. 过程中,记录所有窗口的长度,并返回最大值。
var lengthOfLongestSubstring = function (s) {
    let l = 0;
    let res = 0;
    let map = new Map();
    for (let r = 0; r < s.length; r++) {
        if (map.has(s[r]) && map.get(s[r]) >= l) {
            l = map.get(s[r]) + 1;
        }
        res = Math.max(res, r - l + 1);
        map.set(s[r], r);
    }
    return res;
};
//时间复杂度:O(n) 
//空间复杂度:O(m), m为字符串中不重复字符的个数

六、树

  • 树是一种分层数据的抽象模型。
  • 常见的树包括:DOM树,级联选择,树形控件。
  • JS中没有树,但是可以用Object和Array来构建树。
  • 树的常用操作:深度、广度优先遍历,先中后序遍历。

(一)深度、广度优先遍历

在这里插入图片描述
1.深度优先遍历:
在这里插入图片描述
js实现深度优先遍历:

const tree = {
    val: 'a',
    children: [
        {
            val: 'b',
            children: [
                {
                    val: 'd',
                    children: [],
                },
                {
                    val: 'e',
                    children: [],
                }
            ],
        },
        {
            val: 'c',
            children: [
                {
                    val: 'f',
                    children: [],
                },
                {
                    val: 'g',
                    children: [],
                }
            ],
        }
    ],
};

const dfs = (root) => {
    console.log(root.val);
    root.children.forEach(children=>dfs(children));
    // root.children.forEach(dfs);//上面代码简写
};

dfs(tree);

结果:

a b d e c f g

2.广度优先遍历:
在这里插入图片描述
js实现广度优先遍历:

const tree = {
    val: 'a',
    children: [{
            val: 'b',
            children: [{
                    val: 'd',
                    children: [],
                },
                {
                    val: 'e',
                    children: [],
                }
            ],
        },
        {
            val: 'c',
            children: [{
                    val: 'f',
                    children: [],
                },
                {
                    val: 'g',
                    children: [],
                }
            ],
        }
    ],
};

const bfs = (root) => {
    const q = [root];
    while (q.length > 0) {
        const n = q.shift();
        console.log(n.val);
        // for (let i = 0; i < n.children.length; i++) {
        //     let item = n.children[i];
        //     q.push(item);
        // }
        n.children.forEach(children=>{
            q.push(children);
        });
    }
};

bfs(tree);

输出结果:

a b c d e f g

(二)二叉树

  • 树中每个节点最多只能有两个子节点。
  • 在js中通常用Object来模拟二叉树

例如:

const bt = {
    val: 1,
    left: {
        val: 2,
        left: {
            val: 4,
            left: null,
            right: null,
        },
        right: {
            val: 5,
            left: null,
            right: null,
        },
    },
    right: {
        val: 3,
        left: {
            val: 6,
            left: null,
            right: null,
        },
        right: {
            val: 7,
            left: null,
            right: null,
        },
    },
};

图示:
在这里插入图片描述

(三)二叉树的先序遍历

【重点】:这里前中后序遍历,其实指的就是中间节点的遍历顺序。

思路:

  • 访问根节点
  • 对根节点的左子树进行先序遍历
  • 对根节点的右子树进行先序遍历

方法1:递归

const preorder = (root) => {
    if (!root) { return; }
    console.log(root.val);
    preorder(root.left);
    preorder(root.right);
};

preorder(bt); //1 2 4 5 3 6 7

方法2:利用栈结构

const preorder = (root) => {
    if (!root) { return; }
    const stack = [root];
    while (stack.length) {
        const n = stack.pop();
        console.log(n.val);
        if (n.right) stack.push(n.right);
        if (n.left) stack.push(n.left);
    }
};
preorder(bt); //1 2 4 5 3 6 7

(四)二叉树的中序遍历

思路:

  • 对根节点的左子树进行中序遍历
  • 访问根节点
  • 对根节点的右子树进行中序遍历

方法1:递归

const inorder = (root) => {
    if (!root) { return; }
    inorder(root.left);
    console.log(root.val);
    inorder(root.right);
};
inorder(bt);// 4 2 5 1 6 3 7

方法2:利用栈结构

const inorder = (root) => {
    if (!root) { return; }
    const stack = [];
    let p = root;
    while (stack.length || p) {
        while (p) {
            stack.push(p);
            p = p.left;
        }
        const n = stack.pop();
        console.log(n.val);
        p = n.right;
    }
};
inorder(bt);// 4 2 5 1 6 3 7


//另一个种写法:
const inorder = (root) => {
    if (!root) { return; }
    const stack = [];
    let p = root;
    while (stack.length || p) {
        if (p) {
            stack.push(p);
            p = p.left;
        } else {
        	const n = stack.pop();
        	console.log(n.val);
        	p = n.right;
        }
    }
};
inorder(bt);// 4 2 5 1 6 3 7

(五)二叉树的后续遍历

思路:

  • 对根节点的左子树进行后序遍历
  • 对根节点的右子树进行后序遍历
  • 访问根节点

方法1:递归

const postorder = (root) => {
    if (!root) { return; }
    postorder(root.left);
    postorder(root.right);
    console.log(root.val);
};
postorder(bt);// 4 5 2 6 7 3 1

方法2:利用栈结构

const postorder = (root) => {
    if (!root) { return; }
    const outputStack = [];
    const stack = [root];
    while (stack.length) {
        const n = stack.pop();
        outputStack.push(n);
        if (n.left) stack.push(n.left);
        if (n.right) stack.push(n.right);
    }
    while(outputStack.length){
        const n = outputStack.pop();
        console.log(n.val);
    }
};
postorder(bt);// 4 5 2 6 7 3 1

七、图

图示网络结构的抽象模型,是一组由边连接的节点。

  • 图可以表示任何二元关系,比如道路、航班。。。
  • JS中没有图,但是可以用Object和Array来构建图。
  • 图的表示法:邻接矩阵、邻接表。
  • 图的常用操作:深度、广度优先遍历

(一)图的深度优先遍历

const graph = {
    0: [1, 2],
    1: [2],
    2: [0, 3],
    3: [3]
};

const res = [];
const dfs = (n) => {
    console.log(n); //2 0 1 3
    res.push(n);
    graph[n].forEach(item => {
        // 如果找不到该元素,它将返回-1
        if (res.indexOf(item) === -1) {
            dfs(item);
        }
    });
}
dfs(2);

(二)图的广度优先遍历

 const graph = {
     0: [1, 2],
     1: [2],
     2: [0, 3],
     3: [3]
 };

 const res = [];
 res.push(2);
 const queue = [2];
 while (queue.length > 0) {
     const n = queue.shift();
     console.log(n); // 2 0 3 1
     graph[n].forEach(item => {
         // 如果找不到该元素,它将返回-1
         if (res.indexOf(item) === -1) {
             queue.push(item);
             res.push(item);
         }
     });
 }

八、堆

(一)堆是什么?

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(二)js实现最小堆类

  class MinHeap {
      constructor() {
          this.heap = [];
      }
      getParentIndex(i) {
          return Math.floor((i - 1) / 2);
      }
      getLeftIndex(i) {
          return i * 2 + 1;
      }
      getRightIndex(i) {
          return i * 2 + 2;
      }
      shiftUp(index) {
          if (index === 0) return;
          const parentIndex = this.getParentIndex(index);
          if (this.heap[parentIndex] > this.heap[index]) {
              this.swap(parentIndex, index);
              this.shiftUp(parentIndex);
          }
      }
      shiftDown(index) {
          const leftIndex = this.getLeftIndex(index);
          const rightIndex = this.getRightIndex(index);
          if (this.heap[leftIndex] < this.heap[index]) {
              this.swap(leftIndex, index);
              this.shiftDown(leftIndex);
          }
          if (this.heap[rightIndex] < this.heap[index]) {
              this.swap(rightIndex, index);
              this.shiftDown(rightIndex);
          }
      }
      swap(i1, i2) {
          const temp = this.heap[i1];
          this.heap[i1] = this.heap[i2];
          this.heap[i2] = temp;
      }
      insert(value) {
          this.heap.push(value);
          this.shiftUp(this.heap.length - 1);
      }
      pop() {
          this.heap[0] = this.heap.pop();
          this.shiftDown(0);
      }
      peek() {
          return this.heap[0];
      }
      size() {
          return this.heap.length;
      }
  }

  const h = new MinHeap();

  h.insert(3);
  h.insert(2);
  h.insert(1);
  // console.log(h); // [1, 3, 2]

  h.pop();
  // console.log(h); // [2, 3]

  console.log(h.peek()); //2
  console.log(h.size()); //2

leetcode题目:
在这里插入图片描述
解法1:暴力法

  • 思路:先对数组进行排序,然后找出对应的索引即可。
var findKthLargest = function(nums, k) {
    let res = nums.sort(function(a,b){
        return a-b;
    });
    return res[nums.length-k];
};

解法2:利用堆结构

思路:

  • 1.构建一个最小堆,并依次把数组的值插入堆中。
  • 2.当堆的容量超过K,就删除堆顶。
  • 3.插入结束后,堆顶就是第K个最大的元素。
var findKthLargest = function (nums, k) {
    const h = new MinHeap();
    nums.forEach(c => {
        h.insert(c);
        if (h.size() > k) {
            h.pop();
        }
    });
    return h.peek();
};
// 自己构建一个最小堆类
class MinHeap {
    constructor() {
        this.heap = [];
    }
    getParentIndex(i) {
        return Math.floor((i - 1) / 2);
    }
    getLeftIndex(i) {
        return i * 2 + 1;
    }
    getRightIndex(i) {
        return i * 2 + 2;
    }
    shiftUp(index) {
        if (index === 0) return;
        const parentIndex = this.getParentIndex(index);
        if (this.heap[parentIndex] > this.heap[index]) {
            this.swap(parentIndex, index);
            this.shiftUp(parentIndex);
        }
    }
    shiftDown(index) {
        const leftIndex = this.getLeftIndex(index);
        const rightIndex = this.getRightIndex(index);
        if (this.heap[leftIndex] < this.heap[index]) {
            this.swap(leftIndex, index);
            this.shiftDown(leftIndex);
        }
        if (this.heap[rightIndex] < this.heap[index]) {
            this.swap(rightIndex, index);
            this.shiftDown(rightIndex);
        }
    }
    swap(i1, i2) {
        const temp = this.heap[i1];
        this.heap[i1] = this.heap[i2];
        this.heap[i2] = temp;
    }
    insert(value) {
        this.heap.push(value);
        this.shiftUp(this.heap.length - 1);
    }
    pop() {
        this.heap[0] = this.heap.pop();
        this.shiftDown(0);
    }
    peek() {
        return this.heap[0];
    }
    size() {
        return this.heap.length;
    }
}                                                                                                                                                                                                                                                                               

九、排序和搜索

【注】:https://visualgo.net/此网站用来演示各种排序的动画。

(一)冒泡排序*

思路:

  1. 比较所有相邻的元素,如果第一个比第二个大,则交换他们。
  2. 一轮下来,可以保证最后一个数是最大的。
  3. 以此类推,执行n-1轮,就可以完成排序。

复杂度分析:

  • 时间复杂度:O(n^2)
 Array.prototype.bubbleSort = function () {
     for (let i = 0; i < this.length - 1; i++) {
         for (let j = 0; j < this.length - 1 - i; j++) {
             if (this[j] > this[j + 1]) {
                 const temp = this[j];
                 this[j] = this[j + 1];
                 this[j + 1] = temp;
             }
         }
     }
 }
 const array = [2, 5, 4, 3, 1];
 array.bubbleSort();

(二)选择排序*

思路:

  1. 找到数组中的最小值,选中它并将其放置在第一位。
  2. 接着找到第二小的值,选中它并将其放置在第二位。
  3. 以此类推,执行n-1轮,就可以完成排序。

复杂度分析:

  • 时间复杂度:O(n^2)
 Array.prototype.selectionSort = function () {
     for (let i = 0; i < this.length - 1; i++) {
         let indexMin = i;
         for (let j = i; j < this.length; j++) {
             if (this[indexMin] > this[j]) {
                 this.swap(indexMin, j);
             }
         }
     }
 }
 Array.prototype.swap = function (m, n) {
     const temp = this[m];
     this[m] = this[n];
     this[n] = temp;
 }
 const array = [2, 5, 4, 3, 1];
 array.selectionSort();

(三)插入排序

思路:

  1. 从第二个数开始往前比。
  2. 比他大就往后排。
  3. 以此类推,进行到最后一个数。

复杂度分析:

  • 时间复杂度:O(n^2)
 Array.prototype.insertionSort = function () {
     for (let i = 1; i < this.length; i++) {
         const temp = this[i];
         let j = i;
         while (j > 0) {
             if (this[j - 1] > temp) {
                 this[j] = this[j - 1];
                 j--;
                 this[j] = temp;
             } else {
                 break;
             }
         }
     }
 }
 const array = [2, 5, 4, 3, 1];
 array.insertionSort();

(四)归并排序

思路:

  1. 分:把数组分成两半,再递归地对子数组进行“分”操作,直到分成一个个单独的数。
  2. 合:把两个数合并为有序数组,再对有序数组进行合并,直到全部子数组合并为一个完整的数组。

具体操作:

  1. 新建一个空数组res,用于存放最终排序后的数组。
  2. 比较两个有序数组的头部,较小者出队并推入res中。
  3. 如果两个数组还有值,就重复第二步。

复杂度分析:

  • 分的时间复杂度:O(logn)
  • 合的时间复杂度:O(n)
  • 所以总的时间复杂度:O(n* logn)
 Array.prototype.mergeSort = function () {
     const rec = (array) => {
         if (array.length === 1) return array;
         const mid = Math.floor(array.length / 2);
         const left = array.slice(0, mid);
         const right = array.slice(mid, array.length);
         const orderLeft = rec(left);
         const orderRight = rec(right);
         const res = [];
         while (orderLeft.length || orderRight.length) {
             if (orderLeft.length && orderRight.length) {
                 res.push(orderLeft[0] < orderRight[0] ? orderLeft.shift() : orderRight.shift());
             } else if (orderLeft.length) {
                 res.push(orderLeft.shift());
             } else if (orderRight.length) {
                 res.push(orderRight.shift());
             }
         }
         return res;
     };
     const res = rec(this);
     // 把数组拷贝一份给原来的数组
     res.forEach((n, i) => {
        this[i] = n;
     })
 }
 const array = [2, 5, 4, 3, 1];
 array.mergeSort();

(五)快速排序*

  • 第一种思路:
  1. 分区:从数组中任意选择一个“基准”,所有比基准小的元素放在基准前面,比基准大的元素放在基准的后面。
  2. 递归:递归地对基准前后的子数组进行分区。

复杂度分析:

  • 递归的时间复杂度:O(logn)
  • 分区操作的时间复杂度:O(n)
  • 所以总的时间复杂度:O(n* logn)

代码实现:

 Array.prototype.quickSort = function () {
     const rec = (array) => {
         if (array.length === 0 || array.length === 1) return array;
         const left = [];
         const right = [];
         const mid = array[0];
         for (let i = 1; i < array.length; i++) {
             if (mid > array[i]) {
                 left.push(array[i]);
             } else {
                 right.push(array[i]);
             }
         }
         return [...rec(left), mid, ...rec(right)];
     };
     const res = rec(this);
     // 把数组拷贝一份给原来的数组
     res.forEach((n, i) => {
         this[i] = n;
     })
 }
 const array = [2, 5, 4, 3, 1];
 array.quickSort();
  • 第二种思路:
  • 选择一个元素作为基数(通常是第一个元素),把比基数小的元素放到它左边,比基数大的元素放到它右边(相当于二分),再不断递归基数左右两边的序列。
  • 视频讲解:B站

复杂度分析:

  • 最好: O(n * logn),所有数均匀分布在基数的两边,此时的递归就是不断地二分左右序列。
  • 最坏: O(n²),所有数都分布在基数的一边,此时划分左右序列就相当于是插入排序。
  • 平均: O(n * logn)

代码实现:需加强记忆

var sortArray = function (nums) {
    if (nums.length <= 1) return nums;
    // 递归排序基数左右两边的序列
    function quickSort(arr, left, right) {
        if (left >= right) return;
        let index = partition(arr, left, right);
        quickSort(arr, left, index - 1);
        quickSort(arr, index + 1, right);
        return arr;
    }
    // 将小于基数的数放到基数左边,大于基数的数放到基数右边,并返回基数的位置
    function partition(arr, left, right) {
        // 取第一个数为基数
        let temp = arr[left];
        while (left < right) {
            while (left < right && arr[right] >= temp) {
                right--;
            }
            arr[left] = arr[right];
            while (left < right && arr[left] < temp) {
                left++;
            }
            arr[right] = arr[left];
        }
        // 修改基数的位置
        arr[left] = temp;
        return left;
    }
    quickSort(nums, 0, nums.length - 1);
}

const nums = [19, 97, 9, 17, 1, 8];
sortArray(nums);
console.log(nums); //[ 1, 8, 9, 17, 19, 97 ]

(六)顺序搜索

思路:

  1. 遍历数组。
  2. 找到跟目标值相等的元素,就返回他的下标。
  3. 遍历结束后,如果没有搜索到目标值,就返回-1。

复杂度分析:

  • 时间复杂度:O(n)
 Array.prototype.sequentialSearch = function (item) {
     for (let i = 0; i < this.length; i++) {
         if (item === this[i]) return i;
     }
     return -1;
 }
 const array = [2, 5, 4, 3, 1];
 array.sequentialSearch(1); //4

(七)二分搜索

【注意】:二分搜索的前提是数组是排序好的。

思路:

  1. 从数组的中间元素开始,如果中间元素正好是目标值,则搜索结束。
  2. 如果目标值大于或者小于中间元素,则在大于或小于中间元素的那一半数组中搜索。

复杂度分析:

  • 每一次比较都使得搜索范围缩小一半
  • 时间复杂度:O(logn)
 Array.prototype.binarySearch = function (item) {
     let low = 0;
     let high = this.length - 1;
     while (low <= high) {
         const mid = Math.floor((low + high) / 2);
         const element = this[mid];
         if (element < item) {
             low = mid + 1;
         } else if (element > item) {
             high = mid - 1;
         } else {
             return mid;
         }
     }
     return -1;
 };
 const res = [1, 2, 3, 4, 5].binarySearch(4);
 console.log(res); //3

十、分而治之

  • 分而治之是算法设计中的一种方法。
  • 它将一个问题成多个和原问题相似的小问题,递归解决小问题,再将结果并以解决原来的问题。
  • 举例:
    • 1.归并排序
    • 2.快速排序
    • 3.二分搜索
    • 4.翻转二叉树

LeetCode第 374题:
在这里插入图片描述

// 1.二分搜索
// 时间复杂度:O(logn)
// 空间复杂度:O(1) 
/**
var guessNumber = function (n) {
    let low = 1;
    let high = n;
    while (low <= high) {
        let mid = Math.floor((low + high) / 2);
        const res = guess(mid);
        if (res === 1) {
            low = mid + 1;
        } else if (res === -1) {
            high = mid - 1;
        } else {
            return mid;
        }
    }
};
 */

// 2.分而治之版二分搜索
// 时间复杂度:O(logn)
// 空间复杂度:O(logn) --- 递归
var guessNumber = function (n) {
    const rec = (low, high) => {
        if (low > high) return;
        const mid = Math.floor((low + high) / 2);
        const res = guess(mid);
        if (res === 1) {
            return rec(mid + 1, high);
        } else if (res === -1) {
            return rec(low, mid - 1);
        } else {
            return mid;
        }
    }
    return rec(1, n);
};

LeetCode第 226 题:
在这里插入图片描述

方法1:分而治之
思路:

  1. 分:获取左右子树。
  2. 解:递归地翻转左右子树。
  3. 合:将翻转后的左右子树换个位置放到根节点上。 时间复杂度:O(n) 空间复杂度:O(h):h为树的高度

方法2:利用树的广度优先遍历
思路:

  1. 根节点先入列,然后出列,出列就 “做事”,交换它的左右子节点(左右子树)。
  2. 让左右子节点入列,往后,这些子节点出列,也被翻转。
  3. 直到队列为空,就遍历完所有的节点,翻转了所有子树。
//1.分而治之
var invertTree = function(root) {
    if (root === null) {
        return null;
    }
    const left = invertTree(root.left);
    const right = invertTree(root.right);
    root.left = right;
    root.right = left;
    return root;
};

// 方法1----另一种简单写法:
var invertTree = function(root) {
    if (!root) return null;
    return {
        val:root.val,
        left:invertTree(root.right),
        right:invertTree(root.left)
    }
};



// 2.二叉树的广度优先遍历---队列
var invertTree = function (root) {
    if(root===null) return null;
    const queue = [root];
    while (queue.length > 0) {
        let node = queue.shift();
        [node.left, node.right] = [node.right, node.left]; // 交换左右子树
        if (node.left !== null) {
            queue.push(node.left);
        }
        if (node.right !== null) {
            queue.push(node.right);
        }
    }
    return root;
};



// 3.二叉树的深度优先遍历(先序遍历)---栈
var mirrorTree = function (root) {
    if (root == null) return null;
    const stack = [root];
    while (stack.length > 0) {
        let node = stack.pop();
        if (node.left !== null) stack.push(node.left);
        if (node.right !== null) stack.push(node.right);
        if (node.left !== null || node.right !== null) {
            let temp = node.left;
            node.left = node.right;
            node.right = temp;
        }
    }
    return root;
};

LeetCode第 100 题:
在这里插入图片描述

方法三:分而治之 思路:

  1. 分:获取两个树的左子树和右子树。
  2. 解:递归地判断两个树的左子树是否相同,右子树是否相同。
  3. 合:将上述结果合并,如果根节点的值也相同,树就相同。

时间复杂度:O(n)
空间复杂度:O(n)

//1.投机取巧:直接转为字符串进行比较
/**
var isSameTree = function(p, q) {  
    return JSON.stringify(p)===JSON.stringify(q);
};
 */
// 2.深度优先遍历
/**
var isSameTree = function(p, q) {
    if(p === null && q === null) 
        return true;
    if(p === null || q === null) 
        return false;
    if(p.val !== q.val) 
        return false;
    return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
};
 */
// 3.分而治之
var isSameTree = function (p, q) {
    if (p === null && q === null)
        return true;
    if (p === null || q === null)
        return false;
    const left = isSameTree(p.left,q.left);
    const right = isSameTree(p.right,q.right);
    if (left && right && p.val === q.val)
        return true;
    return false;
};

LeetCode第 101 题:
在这里插入图片描述

方法2:分而治之
思路:

  1. 转化为:判断左右子树是否镜像。
  2. 分解为:树1的左子树和树2的右子树是否镜像,树1的右子树和树2的左子树是否镜像。

解题步骤:

  1. 分:获取两个树的左子树和右子树。
  2. 解:递归地判断树1的左子树和树2的右子树是否镜像,树1的右子树和树2的左子树是否镜像。
  3. 合:如果上述都成立,且根节点的值也相同,则两个树就镜像。

时间复杂度:O(n)
空间复杂度:O(n) —最坏

// 1.正常思路
/**
const check = (p, q) => {
    if (!p && !q) return true;
    if (!p || !q) return false;
    return p.val === q.val && check(p.left, q.right) && check(p.right, q.left);
}
var isSymmetric = function(root) {
    return check(root, root);
};
 */
// 2.分而治之

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

十一、动态规划

  • 动态规划是算法设计中的一种方法。

  • 他将一个问题分解为相互重叠的子问题,通过反复求解子问题,来解决原来的问题。

  • 举例:

    • 斐波那契数列:0 1 1 2 3 5 8 13…
      • 定义子问题:F(n) = F(n-1)+F(n-2); n>=2
      • 反复执行:从2循环到n,执行上述公式。
  • LeetCode第70题
    在这里插入图片描述
    代码:

// 时间复杂度、空间复杂度:O(n)
// var climbStairs = function (n) {
//     if (n === 1) return 1;
//     const dp = [1, 1];
//     for (let i = 2; i <= n; i++) {
//         dp[i] = dp[i - 1] + dp[i - 2];
//     }
//     return dp[n];
// };  
// 优化以上代码
//时间复杂度:O(n),空间复杂度:O(1)
var climbStairs = function(n) {
    let dp0 = 0, dp1 = 1, sum = 1;
    for (let i = 2; i <= n; ++i) {
        dp0 = dp1;
        dp1 = sum;
        sum = dp0 + dp1;
    }
    return sum;
};
  • LeetCode第198题
    在这里插入图片描述
    代码:
// 时间复杂度:O(n)、空间复杂度:O(n)
// var rob = function (nums) {
//     if (nums.length === 0) return 0;
//     const dp = [0, nums[0]];
//     for (let i = 2; i <= nums.length; i++) {
//         dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]);
//     }
//     return dp[nums.length];
// };

// 优化以上代码
// 时间复杂度:O(n)、空间复杂度:O(1)
var rob = function (nums) {
    if (nums.length === 0) return 0;
    let dp0 = 0;
    let dp1 = nums[0];
    for (let i = 2; i <= nums.length; i++) {
        const dp2 = Math.max(dp0 + nums[i - 1], dp1);
        dp0 = dp1;
        dp1 = dp2;
    }
    return dp1;
};
  • LeetCode第53题

在这里插入图片描述

动态规划 基本思路:
f(i)=max{f(i−1)+nums[i],nums[i]}

// 方法一:动态规划
var maxSubArray = function (nums) {
    for (let i = 1; i < nums.length; i++) {
        if (nums[i - 1] > 0) {
            nums[i] += nums[i - 1];
        }
    }
    // console.log(nums);
    return Math.max(...nums);
};

// 方法二:贪心算法
// var maxSubArray = function (nums) {
//     let pre = 0, maxAns = nums[0];
//     nums.forEach((x) => {
//         pre = Math.max(pre + x, x);
//         maxAns = Math.max(maxAns, pre);
//     });
//     return maxAns;
// };
  • LeetCode题目
    在这里插入图片描述
/**
// 动态规划:
// 时间复杂度、空间复杂度:O(n)
var fib = function (n) {
    if (n === 0) return 0;
    const dp = [0, 1]
    for (let i = 2; i <= n; i++) {
        dp[i] = (dp[i - 1] + dp[i - 2]) % 1000000007;
    }
    // console.log(dp);
    return dp[dp.length - 1];
};
 */

// 优化:
// 时间复杂度:O(n),空间复杂度:O(1)
var fib = function (n) {
    if (n === 0) return 0;
    if (n === 1) return 1;
    let dp0 = 0;
    let dp1 = 1;
    let dp2 = null;
    for (let i = 2; i <= n; i++) {
        dp2 = (dp0 + dp1) % 1000000007;
        dp0 = dp1;
        dp1 = dp2;

    }
    return dp2;
};
  • 剑指 Offer 63. 股票的最大利润【经典】
    在这里插入图片描述
//暴力法求解【必会】
var maxProfit = function (prices) {
    let max = 0;
    for (var i = 1; i < prices.length; i++) {
        for (var j = 0; j < i; j++) {
            if (prices[i] > prices[j]) {
                max = Math.max(prices[i] - prices[j],max);
            }
        }
    }
    return max;
};

//动态规划解决:
var maxProfit = function (prices) {
    let min = prices[0];
    let dp = [0];
    for (var i = 1; i < prices.length; i++) {
        dp[i] = Math.max(dp[i - 1], prices[i] - min);
        min = Math.min(min, prices[i]);
    }
    // console.log(dp);
    return dp[dp.length - 1];
};

【总结】:如果子问题是独立的,则用分而治之;如果子问题是相互重叠的,则用动态规划。

十二、贪心算法

  • 贪心算法是算法设计中的一种方法。
  • 期盼通过每个阶段的局部最优选择,从而达到全局的最优。
  • 结果并不一定是最优。
  • 举例:
  • 零钱兑换
    • 输入:coin = [1, 2, 5], amount = 11;
    • 输出:3
    • 解释:11 = 5 + 5 + 1
      ===========================================================
    • 输入:coin = [1, 3, 4], amount = 6;
    • 输出:3
    • 解释:6 = 4 + 1 + 1(这种情况下贪心算法不是最优解,最优解是6 = 3 + 3)
  • LeetCode第455题
    在这里插入图片描述
    代码:
var findContentChildren = function (g, s) {
    g.sort((a, b) => a - b);
    s.sort((a, b) => a - b);
    let i = 0;
    for (let j = 0; j < s.length; j++) {
        if (s[j] >= g[i]) i++;
    }
    return i;
};
  • LeetCode第122题
    在这里插入图片描述
  • 解题思路:
    1.前提:上帝视角,知道未来的价格。
    2.局部最优:见好就收,见差不动。
  • 解题步骤:
    1.新建一个变量,用来统计总利润。
    2.遍历价格数组,如果当前价格比昨天高,就在昨天买,今天卖,否则就不做交易。
    3.遍历结束后,返回所有利润之和。

代码:

// 时间复杂度:O(n)
// 空间复杂度:O(1)
var maxProfit = function (prices) {
    let profit = 0;
    for (let i = 1; i < prices.length; i++) {
        if (prices[i] > prices[i - 1]) {
            profit += prices[i] - prices[i - 1];
        }
    }
    return profit;
};

十三、回溯算法【待重刷】

在这里插入图片描述

  • 回溯算法

回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

  • 回溯法,一般可以解决如下几种问题:
  1. 组合问题:N个数里面按一定规则找出k个数的集合
  2. 切割问题:一个字符串按一定规则有几种切割方式
  3. 子集问题:一个N个数的集合里有多少符合条件的子集
  4. 排列问题:N个数按一定规则全排列,有几种排列方式
  5. 棋盘问题:N皇后,解数独等等
  • 如何理解回溯法
  • 回溯法解决的问题都可以抽象为树形结构!
  • 因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。
  • 在这里插入图片描述
  • 回溯算法题解模板
function backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

解题步骤:

  1. 用递归模拟出所有的情况。
  2. 遇到包含重复元素的情况,就回溯。
  3. 收集所有到达递归终点的情况,并返回。

代码:

// 时间复杂度:O(n!)
// 空间复杂度:O(n)
var permute = function (nums) {
    const res = [];
    const backtrack = (path) => {
        if (path.length === nums.length) {
            res.push(path);
            return;
        }
        nums.forEach(n => {
            if (path.includes(n)) return;
            backtrack(path.concat(n));
        });
    }
    backtrack([]);
    return res;
};
  • LeetCode第78题
    在这里插入图片描述
  • 解题思路:
  1. 要求:(1)所有子集;(2)没有重复元素
  2. 有出路,有死路。
  3. 考虑使用回溯算法
  • 解题步骤:
  1. 用递归模拟出所有的情况。
  2. 保证接的数字都是后面的数字。
  3. 收集所有到达递归终点的情况,并返回。

代码:

// 时间复杂度:O(2^n)
// 空间复杂度:O(n)
var subsets = function (nums) {
    const res = [];
    const backtrack = (path, length, start) => {
        if (path.length === length) {
            res.push(path);
            return;
        }
        for (let i = start; i < nums.length; i++) {
            backtrack(path.concat(nums[i]), length, i + 1);
        }
    }
    for (let i = 0; i <= nums.length; i++) {
        backtrack([], i, 0);
    }
    return res;
};

十四、KMP算法

28. 实现 strStr()
459. 重复的子字符串
知识链接:代码随想录

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值