数据结构笔记

166 阅读9分钟

Chapter1 线性结构

复杂度

O(1) 常数 O(log(n)) 对数 O(n) 线性 O(nlog(n)) 线性和对数乘积 O(n2) 平方 O(2n) 指数

🌏 仅保留最高项,且去除系数

1.1 数组

  1. 数组的扩容:16 -> 18,先申请一个大小翻倍的数组,然后将原来的元素依次复制到新数组中,再向里面添加新元素,然后将多余空间释放掉(性能比较低)
  2. 数组在前面插入(删除)元素:元素位移,将原有的元素一个个向后移,再将新元素插入到最前面,在中间或最前面插入和删除元素性能非常低
  3. 查找数据(index) O(1)

1.2 栈stack

一种受限的线性表,后进先出 仅允许在表的一端进行插入和删除操作,这一端被称为栈顶,另一端称为栈底 进栈,入栈,压栈 出栈,退栈

函数的调用栈:基于栈的调用方式 封装栈结构(基于数组)

1.3 队列

一种受限的线性表,先进先出 仅允许在表的一端(前端/队首)进行删除,另一端(后端/队尾)进行插入操作

事件队列

优先级队列

  • 每个元素具有优先级,插入时会根据优先级放入正确的位置

1.4 链表

数组的内存需要是连续的空间 而链表的元素在内存中不必是连续的 链表不必在创建时就确定大小,并且大小可以无限延申下去 链表在插入和删除数据时,时间复杂度可以达到O(1),相对数组效率高很多 链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(指针)组成 链表访问任何一个元素时都需要从头开始访问,无法通过下标值直接访问元素

双向链表

  • 单向链表:只能从头到尾遍历,链表相连的过程是单向的
  • 双向链表:既可以从头到尾,也可以从尾到头,缺点是插入和删除时需要处理四个引用,占用内存空间更大

Chapter2 哈希表

2.1 哈希表理论

集合

  • 无序,不重复
  • Set类就是集合类
集合间操作
交集
并集
差集
子集

字典

  • 一一对应,键值对,键不可重复值可以重复,键无序

哈希表

  • 数组在进行插入删除的操作时,效率低下(基于索引查询效率高基于内容查找效率低)
  • 通过对数组进行变化,得到了哈希表
  • 哈希表的优点:快速增删查,时间复杂度O(1),速度比树还快,编码比树容易
  • 哈希表相对于数组的不足:没有顺序,key不能重复
  • 它的结构就是数组,神奇之处在于对下标值的一种变换,这种变化称为哈希函数,通过哈希函数可以获取到HashCode

将字符串转成下标值

编码方式:ASCII UTF-8

哈希化

一种压缩方法,将幂的连乘压缩到可接受的数组范围中 压缩方法:取余操作 哈希化:将大数字转化成数组范围下标的过程 哈希函数:单词->大数字->哈希化

冲突

下标值可能会重复,这种情况就叫做冲突 冲突不可避免,我们只能解决冲突 解决冲突的两种方案:1.链地址法(拉链法),开放地址法

链地址法(拉链法)

数组内每个单元存储的都是链表/数组而非简单的元素,查询时先根据哈希化的下标值取出链表再依次查询找寻数据 新数据用的更多则使用链表,新数据使用更少则使用数组更好

开放地址法

寻找空白的位置来放置冲突的数据项 寻找空白位置的方法:

  1. 线性探测
  • 查询时查到空格即返回
  • 删除时不能设置为null(-1)
  1. 二次探测
  • 对探测时的步长进行优化,(线性探测即步长为1),x+12,x+22,x+32...
  • 避免聚集带来的影响
  1. 再哈希法
  • 把关键字用另一个哈希函数,再做一次哈希化,结果作为步长
  • 第二次哈希化不能与第一次哈希函数相同,且结果不能为0
  • stepSize = const - (key%const)
填装因子

填装因子 = 总数据项/哈希表长度 开放地址法最大为1 链地址法最大为∞ 填装因子越大,查询需要的平均次数会呈指数增长 因此,用链地址法更多

优秀的哈希函数
  1. 快速的计算(减少乘除法)
  • 霍纳法则(秦九韶算法) (...(((anx+an-1)x+an-2)x+an-3)x+...+a1)x+a0
  1. 均匀的分布
  • 在使用常量的地方,尽可能使用质数(哈希表的长度,幂计算的底数)
哈希表的扩容
  • 随着LoadFactor增大,效率会不断降低
  • 因此要在合适的时候对哈希表进行扩容,如扩容两倍
如何扩容?
  • 简单扩容:将容量扩大两倍
  • 一旦扩容,所有数据需要重新插入,获取
  • 通常情况下,当LoadFactor大于0.75时就进行扩容

扩容实现:

  1. 建一个oldStorage
  2. 让oldStorage指向this.storage
  3. 让this.storage指向新的Storage
容量质数

实现容量恒为质数 扩容时,将容量*2,然后+1直到新的容量为质数

Chapter3 树结构

3.1 树相关概念

  • 树:n个节点构成的有限集合(当n=0时,称为空树
  • 根(Root):用r表示
  • 其余节点可分为m个互不相交的有限集,称为原来树的子树(SubTree)
  • 节点的度(Degree):节点的子树个数
  • 树的度:树的所有节点的度中最大的度
  • 叶节点:度为0的节点
  • 父节点、子节点
  • 兄弟节点:具有相同父节点的节点
  • 路径和路径长度:路径所包含边的个数就是路径长度
  • 节点的层次:规定根节点所在的层次为1,依次增加
  • 树的深度:最大节点的层次

儿子-兄弟表示法:Data,leftSon,Brother => 二叉树

3.2 二叉搜索树

  • 二叉树:每一个节点最多有两个子节点的树

  • 空树,仅有根节点,仅有左子节点,仅有右子节点,具有两个子节点

  • 一个二叉树第i层最大节点数:2i-1

  • 深度为k的二叉树最大节点总数2k-1

  • 对于任何非空树,度为2的节点数+1=叶节点数

  • 完美二叉树:满二叉树(除叶节点外所有节点都有2个子节点)

  • 完全二叉树:

    • 除二叉树最后一层外,其他各层节点数都达最大。
    • 最后一层左侧叶节点连续存在,只缺右侧若干节点。
  • 二叉搜索树:(BST,Binary Search Tree) BST的性质:可以为空,但不为空时非空左子树所有键值小于根节点的键值,非空右子树所有键值大于根节点的键值 二分查找的思想 查找所需最大次数等于树的深度 插入节点时也是一层层比较大小,找到新节点合适的位置

二叉树的表示方式

  1. 使用数组:完全二叉树好存储也好使用,但非完全二叉树需要转化成完全二叉树
  2. 链表表示:每个节点封装成一个Node,Node包含存储的数据,左右子节点的引用

3.3 树的遍历

 //遍历二叉搜索树
    //先序遍历(根节点=>左子树=>右子树)
    preOrderTraversal = function(handler) {
            this.preOrderTraversalNode(this.root, handler)
        }
        //先序遍历依赖的函数
    preOrderTraversalNode = function(node, handler) {
        if (node != null) {
            //处理经过的节点
            handler(node.key)
                //处理经过节点的左子节点
            this.preOrderTraversalNode(node.left, handler)
                //处理经过节点的右子节点
            this.preOrderTraversalNode(node.right, handler)
        }
    }
​
    //中序遍历(左子树=>根节点=>右子树)
    midOrderTraversal = function(handler) {
            this.midOrderTraversalNode(this.root, handler)
        }
        //中序遍历依赖的函数
    midOrderTraversalNode = function(node, handler) {
        if (node != null) {
            //处理经过节点的左子节点
            this.midOrderTraversalNode(node.left, handler)
                //处理经过的节点
            handler(node.key)
                //处理经过节点的右子节点
            this.midOrderTraversalNode(node.right, handler)
        }
    }
​
    //后序遍历(左子树=>右子树=>根节点)
    postOrderTraversal = function(handler) {
            this.postOrderTraversalNode(this.root, handler)
        }
        //后序遍历依赖的函数
    postOrderTraversalNode = function(node, handler) {
        if (node != null) {
            //处理经过节点的左子节点
            this.postOrderTraversalNode(node.left, handler)
                //处理经过节点的右子节点
            this.postOrderTraversalNode(node.right, handler)
                //处理经过的节点
            handler(node.key)
        }
    }
​
    //搜索最大值和最小值
    max() {
        let node = this.root
            //依次向右寻找直到节点为null
        while (node != null && node.right != null) {
            node = node.right
        }
        return node.key
    }
    min() {
        let node = this.root
            //依次向左寻找直到节点为null
        while (node != null && node.left != null) {
            node = node.left
        }
        return node.key
    }
​
    //搜索特定key
    search(key) {
            return this.searchNode(this.root, key)
        }
        //搜索依赖的函数
    searchNode(node, key) {
        if (node == null) {
            return false
        }
        if (node.key > key) {
            return this.searchNode(node.left, key)
        } else if (node.key < key) {
            return this.searchNode(node.right, key)
        } else {
            return true
        }
    }
​

3.4 二叉搜索树的删除

//删除节点
    remove(key) {
        //1. 寻找要删除的节点
        let current = this.root
        let parent = null
        let isLeft = true
        while (current.key != key) {
            parent = current
            if (key < current.key) {
                current = current.left
                isLeft = true
            } else if (key > node.key) {
                current = current.right
                isLeft = false
            }
            //若没有找到目标节点
            if (current == null) {
                return false
            }
        }
​
        //删除目标节点
        //1. 目标节点没有子节点
        if (current.left == null && current.right == null) {
            //若目标节点同时也是根节点
            if (current == this.root) {
                this.root = null
            } else {
                if (isLeft) {
                    parent.left = null
                } else {
                    parent.right = null
                }
            }
        }
​
        //2. 目标节点有一个子节点
        else if (current.right == null && current.left != null) {
            //若目标节点同时也是根节点
            if (current == this.root) {
                this.root = current.left
            } else {
                if (isLeft) {
                    parent.left = current.left
                } else {
                    parent.right = current.left
                }
            }
        } else if (current.left == null && current.right != null) {
            //若目标节点同时也是根节点
            if (current == this.root) {
                this.root = current.right
            } else {
                if (isLeft) {
                    parent.left = current.right
                } else {
                    parent.right = current.right
                }
            }
        }
​
        //3. 目标节点有两个子节点
        //若用左子树里的节点替代被删的,则用左子树里最大的;若用右子树里的节点替代被删的,则用右子树里最小的。
    }

3.5 二叉树的补充

红黑树

除了符合二叉搜索树的基本规则外

  • 节点是红/黑色
  • 根节点是黑色
  • 叶节点是黑色的空节点
  • 每个红色节点的两个子节点都是黑色
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点

Chapter4 图结构

4.1 图相关概念

4.2 图的表示

4.3 自定义图

4.4 图的遍历

Chapter5 排序&搜索

5.1 简单排序

5.2 高级排序

快速排序

  • 大多数情况下是最快的排序,也是最优的排序
  • 冒泡排序的升级版,分而治之的思想

左指针,右指针,枢纽

左指针向右依次寻找比枢纽大的,然后停下

右指针向左依次寻找比枢纽小的,然后停下

然后交换两个指针指向的值

直到两个指针重叠,交换枢纽与指针指向的值

枢纽的选择:头中尾的中位数

function quick(arr) {
    if (arr.length <= 1) return arr
    let center = Math.floor(arr.length / 2)
    let pivot = arr.splice(center, 1)[0]
    let left = []
    let right = []
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] < pivot) {
            left.push(arr[i])
        } else {
            right.push(arr[i])
        }
    }
    return quick(left).concat([pivot], quick(right))
}
​
let arr = [10, 376, 4, 278, 38, 16, 890, 26, 46]
console.log(quick(arr));

\