算法数据结构

168 阅读12分钟

一、数组

1. 特性

数组是存放在连续内存空间上的相同类型数据的集合。(区别于 TypeScript 中的元组Tuple, 元组中的数据类型可以不同

image.png

从数组在计算机的存储上来看,数组具有以下两个特点:

  • 数组下标都是从0开始的
  • 数组内存空间的地址是连续的

因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。

image.png

2. 时间复杂度

数组操作的时间复杂度分别如下所示:

  • 查找:O(1)
  • 插入:O(n)
  • 删除:O(n)
  • 增加:O(n)

时间复杂度是指随着输入数据的增多,其时间的线性变化情况。

  • 在开始下标插入O(n),在末尾下标插入O(1),取平均值为O(n/2)近似于O(n)
  • 在开始下标删除O(n),在末尾下标删除O(1),取平均值为O(n/2)近似于O(n)

3. 二维数组

二维数组是一种结构较为特殊的数组,只是将数组中的每个元素变成了一维数组。二维数组的本质上仍然是一个一维数组,内部的一维数组仍然从索引0开始,我们可以将它看作一个矩阵,利用二维数组可以处理矩阵相关的问题,如矩阵旋转、对角线遍历,以及对子矩阵的操作等。

对于一个二维数组 A = [[1, 2, 3, 4],[2, 4, 5, 6],[1, 4, 6, 8]],计算机同样会在内存中申请一段连续的空间,并记录第一行数组的索引位置,即 A[0][0] 的内存地址,它的索引与内存地址的关系如下图所示

4. 算法应用

  1. 原地移除数组元素

image.png

var removeElement = function(nums, val) {
    let slow = 0;
    
    for(let fast=0;fast<nums.length;fast++){
        if(nums[fast] !== val){
            nums[slow] = nums[fast];
            slow++;
        }
   }
   
   return slow;
};
  1. 螺旋矩阵

image.png

var generateMatrix = function(n) {
    let startX = 0,startY = 0,offset = 1;
    let loop = Math.floor(n/2);
    let mid = Math.floor(n/2);
    let count = 1;
    let res = Array(n).fill(0).map(()=> new Array(n).fill(0));

    while(loop--){
        let row = startX;
        let clo = startY;
        // 从左到右
        for(;clo < startY+n-offset;clo++){
            res[row][clo] = count ++;
        }
        // 从上到下
        for(;row < startX+n-offset;row++){
            res[row][clo] = count ++;
        }
        // 从右到左
        for(;clo > startY;clo--){
            res[row][clo] = count ++;
        }
        // 从下到上
        for(;row > startX;row--){
            res[row][clo] = count ++;
        }
        startX++;
        startY++;
        offset += 2;
    }
    
    if(n % 2 === 1){
        res[mid][mid] = count;
    }
    
    return res;
};

二、字符串

1. 特性

字符串是具有顺序的字符线性结构,但相比普通数组,它具有更强的语义、更复杂的存储与操作成本,并且在多数高级语言中是不可变的,因此很多看似简单的操作实际上是 O(n)。

2. 时间复杂度

字符串的大多数“增删改”,本质都是 O(n),查找时间复杂度为o(1)

3. 算法应用

  1. 原地反转字符串

image.png

🤔解题思路:

1️⃣创建两个指针,分别从字符串头部和尾部开始

2️⃣for遍历字符串,交换两个指针的数值

var reverseString = function(s) {
    if(s.length <= 1){
        return s;
    }
    let l = 0,r = s.length - 1;

    while(l < r){
        let mid = s[l];
        s[l] = s[r];
        s[r] = mid;
        l++;
        r--;
    }
};

三、链表

1. 特性

链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。

链表的入口节点称为链表的头结点head。

image.png image.png

链表分为单链表、双链表和循环链表。

  • 单链表:指针域只能指向节点的下一个节点

  • 双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。既可以向前查询也可以向后查询。

image.png

  • 循环链表:链表首尾相连

image.png

链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

image.png

2. 时间复杂度

以单链表为例的时间复杂度如下:

  • 查找:O(n)
  • 插入:O(1)
  • 删除:O(1)
  • 增加:O(1)

3. 算法应用

  1. 移除链表元素

image.png

var removeElements = function(head, val) {
    const newHead = new ListNode(-1,head);
    let cur = newHead;

    while(cur.next !== null){
        if(cur.next.val === val){
            cur.next = cur.next.next;
        }else{
            cur = cur.next;
        }
    }
    
    return newHead.next;
};
  1. 反转链表

image.png

var reverseList = function(head) {
    if(!head || !head.next) return head;
    let pre = null,cur = head;

    while(cur){
        const node = cur.next;
        cur.next = pre;
        pre = cur;
        cur = node;
    }
    return pre;
};
  1. 环形链表

image.png image.png

🤔解题思路:

1️⃣初始化快慢指针:slow走一步,fast走两步

2️⃣确定循环条件fast !== null && fast.next !== null

3️⃣判断是否有环fast === slow

4️⃣获取环的入口节点:同步指针分别从头节点和相遇节点开始前进,再次相遇的节点即为入口节点

var detectCycle = function(head) {
    let fast = head,slow = head;

    while(fast !== null && fast.next !== null){
        fast = fast.next.next;
        slow = slow.next;
        // 快慢指针相遇表示有环,需要记录当前相遇节点
        if(fast === slow){
            let index1 = fast;
            let index2 = head;
            // 相遇节点和头节点以相同速度后移,两者相遇的节点即为环入口节点
            while(index1 !== index2){
                index1 = index1.next;
                index2 = index2.next;
            }
            return index1;
        }
    }
    return null;
};

四、Map映射

1. 特性

Map(也叫字典、哈希表)是键值对集合,大部分Map都是无序的,且键唯一性。Map提供以下操作:

  • set(key, value):插入或更新
  • get(key):根据 key 获取 value
  • delete(key):删除 key
  • has(key):判断 key 是否存在
  • keys()/values()/entries():遍历

2. 时间复杂度

  • 查:O(1)
  • 增:O(1)
  • 删:O(1)
  • 改:O(1)
  • 遍历:O(n)

3. 算法应用

  1. 字母异位词分组

image.png image.png

var groupAnagrams = function(strs) {
    const map = new Map();
    for(let s of strs){
        let array = s.split('');
        array.sort();
        let key = array.join('');
        let value = map.get(key) || new Array();
        value.push(s);
        map.set(key,value);
    }
    return Array.from(map.values());
};
  1. 有效的字母异位词

image.png

var isAnagram = function(s, t) {
    if(s.length !== t.length) return false;
    let s_map = {},t_map = {};
    for(let c of s){
        s_map[c] = (s_map[c] || 0) + 1;
    }
    for(let c of t){
        t_map[c] = (t_map[c] || 0) + 1;
    }

    for(let i in s_map){
        if(s_map[i] !== t_map[i]){
            return false;
        }
    }
    return true;
};

五、栈 & 队列

1. 特性

队列是先进先出、栈是先进后出的数据结构。

image.png image.png

2. 时间复杂度

栈和队列的时间复杂度如下:

类型查找插入删除
O(n)O(1)O(1)
队列O(n)O(1)O(1)

3. 算法应用

  1. 有效的括号

image.png

var isValid = function(str) {
    const chartMap = {
        '(':')',
        '[':']',
        '{':'}'
    }
    let stack = [];
    for(let s of str){
        if(chartMap[s]){
            stack.push(s);
        }else{
            const cur = stack.pop();
            if(chartMap[cur] !== s){
                return false;
            }
        }
    }
    return stack.length === 0;
};
  1. 删除相邻重复项

image.png

var removeDuplicates = function(s) {
    const stack = [];
    for(let i=0;i<s.length;i++){
        if(stack.length === 0){
            stack.push(s[i]);
        }else{
            const c = stack.pop();
            if(c === s[i]){
                continue;
            }else{
                stack.push(c);
                stack.push(s[i]);
            }
        }
    }
    return stack.join('');
};
  1. 滑动窗口最大值

image.png

🤔解题关键:

求的是滑动窗口的最大值,如果当前的滑动窗口中有两个下标i和j,其中i在j的左侧(i<j),并且i对应的元素不大于j对应的元素(nums[i]≤nums[j]),当滑动窗口向右移动时,只要i还在窗口中,那么j一定也还在窗口中。因此,由于 nums[j]的存在,nums[i]一定不会是滑动窗口中的最大值,所以可以将 nums[i]永久地移除。

🤔解题思路:

1️⃣初始化一个队列存储所有还没有被移除的下标

2️⃣for循环数组,模拟滑动窗口行为

3️⃣判断队列前几位的大小,踢出小的元素

4️⃣每移动一次窗口就获取一次最大值

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

六、优先队列

1. 特性

Heap数据结构的特点是按照顺序进入队列,按照优先级出队列。进入队列的数据需要记录优先级。

Heap可以分为以下几种:

  • Mini Heap:堆顶元素最小
  • Max Heap:堆顶元素最大

以二叉树为例的小顶堆 image.png 以二叉树为例的大顶堆 image.png

2. 时间复杂度

image.png

3. 算法应用

  1. 数组第K个最大元素

image.png

var findKthLargest = function(nums, k) {
    let heapSize = nums.length;
    // 构建大顶堆
    buildMaxHeap(nums,heapSize);
    // 进行下沉,最大元素下沉到末尾
    for(let i=nums.length-1;i>=nums.length-k+1;i--){
        // 交换元素
        swap(nums,0,i);
        --heapSize;
        // 从左到右、自上而下调整节点
        maxHeapify(nums,0,heapSize);
    }
    return nums[0];
};

var buildMaxHeap = (nums,heapSize)=>{
    for(let i=Math.floor(heapSize/2) - 1;i>=0;i--){
        maxHeapify(nums,i,heapSize);
    }
}

var maxHeapify = (nums,i,heapSize)=>{
    let l = i*2+1;
    let r = i*2+2;
    let largest = i;
    
    if(l < heapSize && nums[l] > nums[largest]){
        largest=l;
    }
    if(r < heapSize && nums[r] > nums[largest]){
        largest=r;
    }
    if(largest !== i){
        swap(nums,i,largest);
        maxHeapify(nums,largest,heapSize);
    }
}

var swap = (a,i,j)=>{
    let temp = a[i];
    a[i] = a[j];
    a[j] = temp;
}

七、树

1. 特性

树是一种非线性的数据结构,由n(n>= 0)个节点组成的集合。

  • n = 0:空树
  • n > 0:有一个根节点root,根节点没有父节点;根节点以外的其他元素被分为m个互不相交的集合T1,T2,T3,...Tm-1,其中每一个集合Ti本身就是一棵树,叫做原树的子树

  1. 节点

每个节点都包含一个数据项和一个指向其他节点的指针(上图1-11都是节点)

  1. 节点的度

一个节点包含指向其他节点的指针个数

  1. 叶节点

节点度为0的节点是叶节点

  1. 分支节点

节点度不为0的节点是分支节点

  1. 子女节点

若节点X有子树,则子树的根节点就是节点X的子女节点,例如2,3,4都是1的子女节点

  1. 父节点

若节点X有子女节点,则节点X就是子女节点的父节点,例如1是2,3,4的父节点

  1. 兄弟节点

同一个父节点的子女节点,互相为兄弟节点,例如2,3,4是兄弟节点

  1. 祖先节点

从根节点到当前节点为止,经过的所有节点,例如11的祖先节点是1,2,6

  1. 子孙节点

一个节点的子女节点、子女节点的子女节点即为该节点的子孙节点,例如2的子孙节点是5,6,11

  1. 节点所在层次

根节点root在第一层,其子女节点在第二层,以此类推

  1. 树的深度

一棵树中距离根节点root最远的节点所在的层次,上述例子中的深度为4

  1. 树的高度

叶节点的高度为1,非叶节点的高度是其子女节点高度的最大值+1,高度和深度的数值相等,但意义和计算方式不相同

  1. 树的度

一棵树中所有节点的度的最大值,例子中的树的度是3

  1. 有序树

一棵树所有节点的子树是按照顺序排列的,T1是第一颗子树,T2是第二颗子树..

  1. 无序树

一棵树所有节点的子树的顺序不重要,可以相互交换位置

  1. 森林

森林是M棵树的集合,M>=0

2. 二叉树

  1. 特点
  • 每个节点最多有两个子女节点,分别称为左子女和右子女
  • 二叉树中不存在度大于2的节点
  • 二叉树的左右子树有次序之分,不能颠倒

二叉树的第n(n>=1)层最多有2的n-1次方个节点

  1. 特殊二叉树
  • 满二叉树

如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。

image.png

  • 完全二叉树:除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第h层,则该层包含 1~ 2^(h-1)个节点。

image.png

  • 二叉搜索树:左子树上所有结点的值均小于它的根结点的值,右子树上所有结点的值均大于根结点的值,且左、右子树也分别为二叉搜索树。

image.png

  • 平衡二叉搜索树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

image.png

二叉树可以链式存储,也可以顺序存储。链式存储可以用指针实现,顺序存储可以用数组实现。

二叉树主要有两种遍历方式:

  • 深度优先遍历:先往深走,遇到叶子节点再往回走
    • 前序遍历(递归法,迭代法)
    • 中序遍历(递归法,迭代法)
    • 后序遍历(递归法,迭代法)
  • 广度优先遍历:一层一层的去遍历
    • 层次遍历(迭代法)

image.png

定义节点类:

const BinTreeNode = function(data){
    this.data = data; // 节点数据项
    this.leftChild = null; // 左子女节点
    this.rightChild = null; // 右子女节点
    this.parentNode = null; // 父节点
}

3. 算法应用

image.png

var preorderTraversal = function(root) {
    const arr = [];
    
    var prevTraversal = function(root){
        if(!root)return;
        arr.push(root.val);
        prevTraversal(root.left);
        prevTraversal(root.right);
    }
    
    prevTraversal(root);
    return arr;
};

image.png

var postorderTraversal = function(root) {
    const arr = [];

    var postTraversal = (root)=>{
        if(root === null)return;
        const left = postTraversal(root.left);
        const right = postTraversal(root.right);
        arr.push(root.val);
    }

    postTraversal(root);
    return arr;
};

image.png

var inorderTraversal = function(root) {
    const arr = [];
    
    var inTraversal = function(node){
        if(!node)return;
        inTraversal(node.left);
        arr.push(node.val);
        inTraversal(node.right);
    }

    inTraversal(root);
    return arr;
};

image.png

var levelOrder = function(root) {
    let arr = [],queue = [root];
    if(root === null){
        return arr;
    }

    while(queue.length){
        // 存放每一层的节点
        let curLevel = [];
        let length = queue.length;
        for(let i=0;i < length;i++){
            let node = queue.shift();
            curLevel.push(node.val);
            if(node.left)queue.push(node.left);
            if(node.right)queue.push(node.right)
        }
        // 把每一层的结果放入数组中
        arr.push(curLevel);
    }
    
    return arr;
};

image.png

var invertTree = function(root) {
    if(!root) return null;
    const left = invertTree(root.left);
    const right = invertTree(root.right);
    root.left = right;
    root.right = left;
    return root;
};

image.png

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