前言
【从蛋壳到满天飞】JS 数据结构解析和算法实现,全部文章大概的内容如下: Arrays(数组)、Stacks(栈)、Queues(队列)、LinkedList(链表)、Recursion(递归思想)、BinarySearchTree(二分搜索树)、Set(集合)、Map(映射)、Heap(堆)、PriorityQueue(优先队列)、SegmentTree(线段树)、Trie(字典树)、UnionFind(并查集)、AVLTree(AVL 平衡树)、RedBlackTree(红黑平衡树)、HashTable(哈希表)
源代码有三个:ES6(单个单个的 class 类型的 js 文件) | JS + HTML(一个 js 配合一个 html)| JAVA (一个一个的工程)
全部源代码已上传 github,点击我吧,光看文章能够掌握两成,动手敲代码、动脑思考、画图才可以掌握八成。
本文章适合 对数据结构想了解并且感兴趣的人群,文章风格一如既往如此,就觉得手机上看起来比较方便,这样显得比较有条理,整理这些笔记加源码,时间跨度也算将近半年时间了,希望对想学习数据结构的人或者正在学习数据结构的人群有帮助。
堆和优先队列(HeapAndPriorityQueue)
- 使用了二分搜索树实现了集合和映射这两个相对来讲更加高层的数据结构
- 树这种数据结构本身在计算机科学领域占有重要的地位,
- 树这种形状本身可以产生非常多的拓展,
- 在面对不同的问题的时候可以稍微改变或者限制树这种数据结构的性质,
- 从而产生不同的数据结构,高效的解决不同的问题,
- 有四个不同的例子,堆、线段树、字典树、并查集,
- 通过这些不同的数据结构的学习可以体会到数据结构的灵活之处,
- 以及在设计数据结构的时候其中的一些思考非常重要,
- 因为这些思考会让你对数据结构这个领域有更加深刻的认识。
- 堆是一个特殊的树结构
优先队列(PriorityQueue)
- 优先队列本身就是一种队列
- 普通队列:先进先出、后进后出
- 如排队买票吃饭一样。
- 优先队列:出队顺序和入队顺序无关,和优先级相关
- 如去医院看病,床位非常的紧张,
- 需要排队才能去做手术的话,
- 此时做手术的这个队伍的顺序就是一个优先队列,
- 因为医生是根据每一个患者的病症的不同以及需要这个手术的紧急程度的不同
- 来去安排谁先做手术谁后做手术,
- 也可以认为每一个患者是有一个优先级的,
- 是优先级高着先得,这样的一个队列就叫做优先队列,
- 它和普通队列最主要的区别是在于出队这个操作上,
- 入队很简单,但是优先级高的先出队。
- 优先队列的应用
- 在计算机的世界中优先队列的使用也是非常多的,
- 例如操作系统中进行任务的调度,
- 现在的操作系统中会同时执行多个任务,
- 操作系统就要为这多个任务分配计算资源,
- 包括去分配 cpu 的时间片,具体去分配这些资源的时候,
- 操作系统就要看各个任务的优先级,
- 然后动态的去选择优先级最高的任务来执行。
- 这个动态很重要,如果任务数量是固定的,
- 那么就不需要去制作新的数据结构来处理这个问题,
- 那么这个过程就只需要一个排序算法而并不是一个优先队列,
- 通常实际情况不是这样的,
- 假设有一个任务处理中心,有三个任务请求过来,
- 那么这个时候任务处理中心就需要去找出优先级最高的那个请求,
- 然后对这个任务进行相应的处理,但是处理完这个任务的同时,
- 很有可能就来了很多新的任务请求,这就是动态的意思,
- 并不能够在一开始就确定任务中心一共需要处理多少个任务,
- 其实医生是不知道每天或者每个月每个季度每一年要来多少患者,
- 它要随时根据新来的患者的情况来调整整个队列的优先级,
- 这就是动态的意思,而不是一开始任务中心就知道所有的任务是什么,
- 然后排排序就好了,就像医生也不能一开始就把今年要做的所有的手术都列出来,
- 然后排排序就好了,随着时间的推移会不停的有新的元素入队,
- 也就是队列中的元素是在不断的变化的,
- 所以必须使用优先队列这样的数据结构来解决这个问题,
- 而不仅仅是按照优先级排序。
- 比如做一个简单的 AI
- 这个 AI 要自动的帮你打怪,其实 AI 也没有那么高级,
- 在很多游戏的底层本身就存在这样的 AI,
- 比如说在一个即时战略的游戏中,当你创建了己方军队的时候,
- 当然可以通过自己的操作来指挥己方的军队去攻击哪些敌人,
- 但是你不去指定,当敌方军队接近己方军队的时候,
- 己方军队也会自动的去攻击敌方军队,这也叫做一个 AI,
- 这种 AI 其实是非常常见的,这种时候 AI 可能同时面对不同的敌人,
- 那么它就需要选择优先去打那种敌人,在这种情况下就需要使用优先队列,
- 其实就是去打威胁程度最高的敌人,也就是去打优先级最高的那个敌人,
- 优先级的高低是可以定义的,可能是最强悍的那个敌人、
- 也有可能是最弱小的敌人、也有可能是距离你最近的敌人等等,
- AI 自动打怪中的优先队列也是动态的处理敌人,
- 因为敌人是不断的在接近你,在每一时刻 AI 都需要考虑新的敌人的出现,
- 因为新的敌人可能是优先级更高的需要被打击的目标,
- 这就是一个动态的过程,所以才需要使用优先队列。
- 实现优先级队列的时候是不用去管优先高低的
- 当用户具体去使用的时候,才会去定义优先级。
优先队列的实现思路
MyQueue
void enqueue(e)
E dequeue()
E getFront()
int getSize()
boolean isEmpty()
MyPriorityQueue
- 实现的时候与普通队列的实现会有些区别,
- 主要区别在于出队操作和获取队首元素操作上,
- 因为出队元素是优先级最高的元素,
- 队首的元素也是优先级最高的元素,
- 并不是普通队列那样的选择最早进入队列的元素,
- 对于优先队列这样的数据结构,
- 也是可以使用不同的底层实现的,
- 可以使用最基础的普通线性数据结构和顺序线性结构来实现,
- 也可以使用
堆
来进行实现。
两种基础实现优先队列思路
- 使用普通的线性结构
- 入队为
O(1)
级别的操作 - 出队需要扫描一遍线性结构中所有的元素,
- 从而找出其优先级最高的那个元素,
- 然后把它拿出队列,
- 所以为
O(n)
级别的操作, - 如果某一操作为
O(n)
级别的操作, - 那么就会大大的降低整个数据结构的效率。
- 所以普通线性结构的实现方式性能方面会很不好。
- 入队为
- 顺序线性结构
- 整个线性结构维持所有元素的顺序,
- 整个线性数据结构都是从小到大或者从大到小排列的,
- 在这种情况下出队将变得非常的容易,
- 出队会是
O(1)
级别的操作, - 因为只需要拿出当前这个数据结构队首
- 或者队尾的那个元素就好了,
- 那么入队的时候就会是一个
O(n)
级别的操作了, - 在入队的时候需要找到这个元素在线性结构中应该插入的位置,
- 这个找到合适插入位置的操作需要
O(n)
的复杂度, - 在最差的情况就需要将整个线性数据结构都扫描一遍,
- 所以顺序线性结构在出队的操作上是
O(1)
的复杂度, - 但是在入队的操作上是
O(n)
的复杂度。 - 所以无论是普通的线性结构还是顺序的线性结构,
- 它们都有一定的劣势,都会存在一个操作是
O(n)
级别的操作, - 它们都不够好。
- 可以使用动态数组、链表这样的底层实现来进行优先队列的实现
- 虽然这样实现出来的优先队列,可能实际不会去应用,
- 但是也是一个很好的练习,可以深入的理解队列这样抽象的数据结构,
- 在这个基础上限制它的性质,就创建出了优先队列这个概念,
- 具体在实现优先队列这个概念的时候,还可以使用不同的底层实现,
- 这在数据结构领域是非常重要的思想。
使用堆来实现优先队列思路
- 使用堆这种数据结构
- 入队操作和出队操作都是
O(logn)
这种级别的操作, - 而且它和二分搜索树不同,
- 二分搜索树是在平均情况下是
O(logn)
的时间复杂度, - 而堆是在最差的情况下是
O(logn)
的时间复杂度, - 这也使得堆这种数据结构是相当高效的。
- 入队操作和出队操作都是
- 它和前面两种基础的实现方式有着天壤之别。
堆(Heap)
- 在计算机科学的领域通常你见到了
O(logn)
这样的时间复杂度- 那么近乎一定就和树这样的数据结构有关,
- 并不一定是显示的构造出一棵树,
- 无论是排序算法中的归并排序、快速排序都是
O(nlog(n))
这个级别的, - 在排序的过程中没有使用树这种数据结构,
- 但是这个递归的过程中其实形成了一棵隐形的递归树。
堆的基本结构
-
堆这种结构本身也是一棵树
- 其实堆也有很多种,
- 堆最为主流的一种实现方式是使用二叉树来表示一个堆,
- 也叫二叉堆(BinaryHeap),
- 说白了二叉堆就是满足一些特殊性质的二叉树。
-
满二叉树与完全二叉树
- 满二叉树,就是除了叶子节点之外,
- 所有的节点的左右孩子都不为空。
- 完全二叉树不一定是一个满的二叉树,但是它不满的那部分,
- 也就是在缺失节点的那部分一定是在整颗树的右下侧,
- 也就是说把元素按照一层一层的顺序排列成
- 一棵二叉树的形状的时候,得到的这棵树就是完全的二叉树。
- 一个三层的满二叉树能装 7 个节点,一个四层的满二叉树能装 15 个节点,
- 10 个节点对于一棵完全二叉树来说,前七节点装满前三层,
- 对于第四层则是从左到右把剩下的三个节点放进去,
- 这就是完全二叉树的定义。
-
教材中对于完全二叉树的定义非常的拗口
- 其实完全二叉树非常的简单的,
- 就是把元素一层一层的放置,直到放不下了为止,
- 所有整棵树的右下角的这部分可能是空的,因为缺少一些元素。
-
二叉堆满足的性质
- 首先它是一棵完全二叉树,
- 除此之外它还有一个非常重要的性质,
- 在堆(树)中的某个节点的值或者任意一个节点的值
- 总是不大于其父节点的值,
- 也就是说所有节点的值一定大于或者等于它的孩子节点的值,
- 所以根节点的元素一定是最大的元素,它大于它所有左右节点的值,
- 同时它的左右子树也是一个堆,对于树中任意节点都会满足这个性质,
- 这样得到的堆通常叫做最大堆,因为根节点是最大的一个元素,
- 相应的也可以定义出最小堆,这个定义方式和最大堆一样,
- 只不过每一个节点的值都要小于等于它的孩子节点的值,
- 这样得到的堆叫做最小堆,
- 最大堆和最小堆在某种程度上是可以统一的,
- 因为什么叫大什么叫小可以自己来定义。
- 虽然在堆中每一个节点的值都大于等于它的左右孩子节点的值
- 但是在堆这种数据结构上,
- 层次比较低的节点的值不一定大于层次比较高的节点的值,
- 因为二叉堆只保证每一个节点的父节点比自己大,
- 但是节点的大小和节点所在的层次其实没有必然的联系。
-
实现二叉堆必须满足的要求,
- 首先是完全二叉树,
- 其次对于堆中每一个节点相应的元素值都是大于等于它的孩子节点的,
- 这样得到的就是最大堆,最大堆是一个完全二叉树,所以在具体实现上,
- 有一个很巧妙的手段,可以使用二分搜索树的方式,
- 先定义一个节点然后定义它的左右孩子就能实现这个堆了,
- 但是完全二叉树的特点其实就是一个一个的节点
- 按照顺序一层一层的码放出来。
- 可以使用数组的方式表现出一颗完全二叉树,
- 对于数组的表示方式来说,要解决的问题是,
- 在数组中的每一个节点应该怎么找到它的左右孩子,
- 因为设置一个节点的话直接使用这个节点左右的指针
- 去指向左右孩子所在的节点,但是用数组去存储的话,
- 其实存在一些规律。
-
以数组的方式表现一棵完全二叉树的规律
parent(i) = i / 2
,- 这个节点的父节点所在位置的索引就是当前节点二分之一(忽略小数或向下取整),
left child (i) = 2 * i
,- 这个节点的左孩子所在位置的索引就是当前节点索引的 2 倍,
right child (i) = 2 * i + 1
。- 这个节点的右孩子所在位置的索引就是当前节点索引的 2 倍+1,
- 这样一来不仅可以非常方便的去索引这个节点的左右孩子,
- 还可以非常方便的去索引到当前节点的父节点,
- 这就是以数组的方式去表现一棵完全二叉树的规律和性质。
-
完全二叉树的好处
- 它本身就是那些元素按照顺序的一层一层的在这棵树中排列,
- 所以将它标上索引之后,
- 每一个节点和它的左右孩子节点以及它的父节点在索引之间
- 就存在了一种很明显的逻辑关系,
- 这个逻辑关系对于完全二叉树来说是成立的。
-
在很多教科书中实现堆的时候,
- 用数组来存储,索引都是从 1 开始标,
- 这是因为相对来说,计算孩子节点和父亲节点会比较方便,
- 这样一来就会出现一个小问题,也就是将 0 这个位置空出来了,
- 但是空出来并没有什么影响,例如在循环队列中、虚拟头节点链表中,
- 也都诚心的空出这么一个位置来方便逻辑的编写,
- 不过对于堆来说,就算不空出这个位置,逻辑一样是非常简单的,
- 区别在于计算父节点和左右孩子节点索引时相应的公式发生了一点点的改变,
- 也就是相应的 i 进行了偏移
// 原来是这样的 空了数组中索引为0的位置 parent(i) = i / left child (i) = * i right child (i) = * i + // 偏移之后是这样的 没空数组中索引为0的位置 parent(i) = (i - ) / left child (i) = * i + right child (i) = * i + 复制代码
实现最大堆基本架构 代码示例
-
(class: Myarray, class: MaxHeap)
-
Myarray
// 自定义类 class MyArray { // 构造函数,传入数组的容量capacity构造Array 默认数组的容量capacity=10 constructor(capacity = 10) { this.data = new Array(capacity); this.size = ; } // 获取数组中的元素实际个数 getSize() { return this.size; } // 获取数组的容量 getCapacity() { return this.data.length; } // 判断数组是否为空 isEmpty() { return this.size === ; } // 给数组扩容 resize(capacity) { let newArray = new Array(capacity); for (var i = ; i < this.size; i++) { newArray[i] = this.data[i]; } // let index = this.size - 1; // while (index > -1) { // newArray[index] = this.data[index]; // index --; // } this.data = newArray; } // 在指定索引处插入元素 insert(index, element) { // 先判断数组是否已满 if (this.size == this.getCapacity()) { // throw new Error("add error. Array is full."); this.resize(this.size * ); } // 然后判断索引是否符合要求 if (index < || index > this.size) { throw new Error( 'insert error. require index < 0 or index > size.' ); } // 最后 将指定索引处腾出来 // 从指定索引处开始,所有数组元素全部往后移动一位 // 从后往前移动 for (let i = this.size - ; i >= index; i--) { this.data[i + ] = this.data[i]; } // 在指定索引处插入元素 this.data[index] = element; // 维护一下size this.size++; } // 扩展 在数组最前面插入一个元素 unshift(element) { this.insert(, element); } // 扩展 在数组最后面插入一个元素 push(element) { this.insert(this.size, element); } // 其实在数组中添加元素 就相当于在数组最后面插入一个元素 add(element) { if (this.size == this.getCapacity()) { // throw new Error("add error. Array is full."); this.resize(this.size * ); } // size其实指向的是 当前数组最后一个元素的 后一个位置的索引。 this.data[this.size] = element; // 维护size this.size++; } // get get(index) { // 不能访问没有存放元素的位置 if (index < || index >= this.size) { throw new Error('get error. index < 0 or index >= size.'); } return this.data[index]; } // 扩展: 获取数组中第一个元素 getFirst() { return this.get(); } // 扩展: 获取数组中最后一个元素 getLast() { return this.get(this.size - ); } // set set(index, newElement) { // 不能修改没有存放元素的位置 if (index < || index >= this.size) { throw new Error('set error. index < 0 or index >= size.'); } this.data[index] = newElement; } // contain contain(element) { for (var i = ; i < this.size; i++) { if (this.data[i] === element) { return true; } } return false; } // find find(element) { for (var i = ; i < this.size; i++) { if (this.data[i] === element) { return i; } } return ; } // findAll findAll(element) { // 创建一个自定义数组来存取这些 元素的索引 let myarray = new MyArray(this.size); for (var i = ; i < this.size; i++) { if (this.data[i] === element) { myarray.push(i); } } // 返回这个自定义数组 return myarray; } // 删除指定索引处的元素 remove(index) { // 索引合法性验证 if (index < || index >= this.size) { throw new Error('remove error. index < 0 or index >= size.'); } // 暂存即将要被删除的元素 let element = this.data[index]; // 后面的元素覆盖前面的元素 for (let i = index; i < this.size - ; i++) { this.data[i] = this.data[i + ]; } this.size--; this.data[this.size] = null; // 如果size 为容量的四分之一时 就可以缩容了 // 防止复杂度震荡 if (Math.floor(this.getCapacity() / ) === this.size) { // 缩容一半 this.resize(Math.floor(this.getCapacity() / )); } return element; } // 扩展:删除数组中第一个元素 shift() { return this.remove(); } // 扩展: 删除数组中最后一个元素 pop() { return this.remove(this.size - ); } // 扩展: 根据元素来进行删除 removeElement(element) { let index = this.find(element); if (index !== ) { this.remove(index); } } // 扩展: 根据元素来删除所有元素 removeAllElement(element) { let index = this.find(element); while (index != ) { this.remove(index); index = this.find(element); } // let indexArray = this.findAll(element); // let cur, index = 0; // for (var i = 0; i < indexArray.getSize(); i++) { // // 每删除一个元素 原数组中就少一个元素, // // 索引数组中的索引值是按照大小顺序排列的, // // 所以 这个cur记录的是 原数组元素索引的偏移量 // // 只有这样才能够正确的删除元素。 // index = indexArray.get(i) - cur++; // this.remove(index); // } } // @Override toString 2018-10-17-jwl toString() { let arrInfo = `Array: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`; arrInfo += `data = [`; for (var i = ; i < this.size - ; i++) { arrInfo += `${this.data[i]}, `; } if (!this.isEmpty()) { arrInfo += `${this.data[this.size - ]}`; } arrInfo += `]`; // 在页面上展示 document.body.innerHTML += `${arrInfo}<br /><br /> `; return arrInfo; } } 复制代码
-
MaxHeap
// 自定义二叉堆之最大堆 class MyMaxHeap { constructor(capacity = 10) { this.myArray = new MyArray(capacity); } // 辅助函数 计算出堆中指定索引位置的元素其父节点的索引 - calcParentIndex(index) { if (index === ) // 索引为0是根节点,根节点没有父亲节点,小于0就更加不可以了 throw new Error("index is 0. doesn't have parent."); return Math.floor((index - ) / ); } // 辅助函数 计算出堆中指定索引位置的元素其左孩子节点的索引 - calcLeftChildIndex(index) { return index * + ; } // 辅助函数 计算出堆中指定索引位置的元素其右孩子节点的索引 - calcRightChildIndex(index) { return index * + ; } // 获取堆中实际的元素个数 getSize() { return this.myArray.getSize(); } // 返回堆中元素是否为空的判断值 isEmpty() { return this.myArray.isEmpty(); } } 复制代码
向堆中添加元素 和 Sift Up
- 最大堆是一个完全二叉树
- 所以可以非常方便使用数组的方式来表示它。
- 向堆中添加元素
- 从用户的角度上来看是添加元素,
- 但是从堆的角度上来看,
- 会涉及到堆的一个非常基础的内部操作,
- 也就是 Sift Up(堆中元素上浮的过程),
- 添加操作一定是往堆的最底层进行添加,
- 也就是向数组的尾部进行添加,
- 新添加的元素会与其父祖节点进行比较,
- 如果新添加的元素比父祖辈元素大,
- 则会不断的交换元素,直到满足完全二叉树的性质,
- 也就是每一个节点都比其孩子节点大,也比其父节点小,
- 这个不断交换元素,就是元素在堆中慢慢从底层
- 上浮到合适层的过程就叫 Sift UP,
- 计算父祖辈元素的索引可以通过新增加的元素的索引来进行计算,
- 也就是数组中实际个数减去一获得。
- SiftUp 操作无论是递归写法还是非递归写法,他们都一个共同的特点
- 元素上浮后结束的条件 当前节点元素值 小于其父节点元素值
- 索引越界的终止条件 要上浮的元素索引 小于等于 0
- 有了这两个条件之后,实现上浮操作很简单。
代码示例
-
(class: Myarray, class: MaxHeap)
-
Myarray
// 自定义类 class MyArray { // 构造函数,传入数组的容量capacity构造Array 默认数组的容量capacity=10 constructor(capacity = 10) { this.data = new Array(capacity); this.size = ; } // 获取数组中的元素实际个数 getSize() { return this.size; } // 获取数组的容量 getCapacity() { return this.data.length; } // 判断数组是否为空 isEmpty() { return this.size === ; } // 给数组扩容 resize(capacity) { let newArray = new Array(capacity); for (var i = ; i < this.size; i++) { newArray[i] = this.data[i]; } // let index = this.size - 1; // while (index > -1) { // newArray[index] = this.data[index]; // index --; // } this.data = newArray; } // 在指定索引处插入元素 insert(index, element) { // 先判断数组是否已满 if (this.size == this.getCapacity()) { // throw new Error("add error. Array is full."); this.resize(this.size * ); } // 然后判断索引是否符合要求 if (index < || index > this.size) { throw new Error( 'insert error. require index < 0 or index > size.' ); } // 最后 将指定索引处腾出来 // 从指定索引处开始,所有数组元素全部往后移动一位 // 从后往前移动 for (let i = this.size - ; i >= index; i--) { this.data[i + ] = this.data[i]; } // 在指定索引处插入元素 this.data[index] = element; // 维护一下size this.size++; } // 扩展 在数组最前面插入一个元素 unshift(element) { this.insert(, element); } // 扩展 在数组最后面插入一个元素 push(element) { this.insert(this.size, element); } // 其实在数组中添加元素 就相当于在数组最后面插入一个元素 add(element) { if (this.size == this.getCapacity()) { // throw new Error("add error. Array is full."); this.resize(this.size * ); } // size其实指向的是 当前数组最后一个元素的 后一个位置的索引。 this.data[this.size] = element; // 维护size this.size++; } // get get(index) { // 不能访问没有存放元素的位置 if (index < || index >= this.size) { throw new Error('get error. index < 0 or index >= size.'); } return this.data[index]; } // 扩展: 获取数组中第一个元素 getFirst() { return this.get(); } // 扩展: 获取数组中最后一个元素 getLast() { return this.get(this.size - ); } // set set(index, newElement) { // 不能修改没有存放元素的位置 if (index < || index >= this.size) { throw new Error('set error. index < 0 or index >= size.'); } this.data[index] = newElement; } // contain contain(element) { for (var i = ; i < this.size; i++) { if (this.data[i] === element) { return true; } } return false; } // find find(element) { for (var i = ; i < this.size; i++) { if (this.data[i] === element) { return i; } } return ; } // findAll findAll(element) { // 创建一个自定义数组来存取这些 元素的索引 let myarray = new MyArray(this.size); for (var i = ; i < this.size; i++) { if (this.data[i] === element) { myarray.push(i); } } // 返回这个自定义数组 return myarray; } // 删除指定索引处的元素 remove(index) { // 索引合法性验证 if (index < || index >= this.size) { throw new Error('remove error. index < 0 or index >= size.'); } // 暂存即将要被删除的元素 let element = this.data[index]; // 后面的元素覆盖前面的元素 for (let i = index; i < this.size - ; i++) { this.data[i] = this.data[i + ]; } this.size--; this.data[this.size] = null; // 如果size 为容量的四分之一时 就可以缩容了 // 防止复杂度震荡 if (Math.floor(this.getCapacity() / ) === this.size) { // 缩容一半 this.resize(Math.floor(this.getCapacity() / )); } return element; } // 扩展:删除数组中第一个元素 shift() { return this.remove(); } // 扩展: 删除数组中最后一个元素 pop() { return this.remove(this.size - ); } // 扩展: 根据元素来进行删除 removeElement(element) { let index = this.find(element); if (index !== ) { this.remove(index); } } // 扩展: 根据元素来删除所有元素 removeAllElement(element) { let index = this.find(element); while (index != ) { this.remove(index); index = this.find(element); } // let indexArray = this.findAll(element); // let cur, index = 0; // for (var i = 0; i < indexArray.getSize(); i++) { // // 每删除一个元素 原数组中就少一个元素, // // 索引数组中的索引值是按照大小顺序排列的, // // 所以 这个cur记录的是 原数组元素索引的偏移量 // // 只有这样才能够正确的删除元素。 // index = indexArray.get(i) - cur++; // this.remove(index); // } } // 新增: 交换两个索引位置的变量 2018-11-6 swap(indexA, indexB) { if ( indexA < || indexA >= this.size || indexB < || indexB >= this.size ) throw new Error('Index is Illegal.'); // 索引越界异常 let temp = this.data[indexA]; this.data[indexA] = this.data[indexB]; this.data[indexB] = temp; } // @Override toString 2018-10-17-jwl toString() { let arrInfo = `Array: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`; arrInfo += `data = [`; for (var i = ; i < this.size - ; i++) { arrInfo += `${this.data[i]}, `; } if (!this.isEmpty()) { arrInfo += `${this.data[this.size - ]}`; } arrInfo += `]`; // 在页面上展示 document.body.innerHTML += `${arrInfo}<br /><br /> `; return arrInfo; } } 复制代码
-
MaxHeap
// 自定义二叉堆之最大堆 class MyMaxHeap { constructor(capacity = 10) { this.myArray = new MyArray(capacity); } // 添加操作 add(element) { // 追加元素 this.myArray.push(element); // 将追加的元素上浮到堆中合适的位置 this.siftUp(this.myArray.getSize() - ); } // 堆的上浮操作 - siftUp(index) { // this.nonRecursiveSiftUp(index); this.recursiveSiftUp(index); // 无论是递归还是非递归都有一个 // 元素上浮后结束的条件 当前节点元素值 小于其父节点元素值 // 和 // 索引即将越界的终止条件 要上浮的元素索引 小于等于0 } // 堆的上浮操作 递归算法 - recursiveSiftUp(index) { // 解决最基本的问题, 递归终止条件 if (index <= ) return; let currentValue = this.myArray.get(index); let parentIndex = this.calcParentIndex(index); let parentValue = this.myArray.get(parentIndex); // 递归写法 if (this.compare(currentValue, parentValue) > ) { this.swap(index, parentIndex); this.recursiveSiftUp(parentIndex); } } // 堆的上浮操作 非递归算法 - nonRecursiveSiftUp(index) { if (index <= ) return; let currentValue = this.myArray.get(index); let parentIndex = this.calcParentIndex(index); let parentValue = this.myArray.get(parentIndex); while (this.compare(currentValue, parentValue) > ) { // 交换堆中两个元素位置的值 this.swap(index, parentIndex); // 交换了位置之后,元素上浮后的索引变量也要进行相应的变更 index = parentIndex; // 如果索引小于等于0了 那就结束循环 if (index <= ) break; currentValue = this.myArray.get(index); parentIndex = this.calcParentIndex(index); parentValue = this.myArray.get(parentIndex); } } // 堆中两个元素的位置进行交换 swap(indexA, indexB) { this.myArray.swap(indexA, indexB); } // 辅助函数 计算出堆中指定索引位置的元素其父节点的索引 - calcParentIndex(index) { if (index === ) // 索引为0是根节点,根节点没有父亲节点,小于0就更加不可以了 throw new Error("index is 0. doesn't have parent."); return Math.floor((index - ) / ); } // 辅助函数 计算出堆中指定索引位置的元素其左孩子节点的索引 - calcLeftChildIndex(index) { return index * + ; } // 辅助函数 计算出堆中指定索引位置的元素其右孩子节点的索引 - calcRightChildIndex(index) { return index * + ; } // 比较的功能 - compare(elementA, elementB) { if (elementA === null || elementB === null) throw new Error("element is error. element can't compare."); if (elementA > elementB) return ; else if (elementA < elementB) return ; else return ; } // 获取堆中实际的元素个数 getSize() { return this.myArray.getSize(); } // 返回堆中元素是否为空的判断值 isEmpty() { return this.myArray.isEmpty(); } } 复制代码
取出堆中的最大元素和 Sift Down
- 从堆中取出元素也就是从堆的顶部取出元素
- 在最大堆中堆顶的元素就是最大元素,
- 也就是堆中根节点的元素,这个过程叫做 Extract Max,
- 也就是提取出堆中最大的元素
- 从堆中取出元素
- 取出了堆顶的元素后,
- 堆中就有两棵子树了,就不符合一棵完全二叉树的性质了,
- 这时候就让最低层的最后一个元素放到最上层的根节点,
- 那么又开始符合一棵完全二叉树的性质了,
- 只不过根节点的元素不一定符合父节点的值大于所有子节点的值的性质,
- 也就是不符合堆的性质了,因为每一个节点要大于等于其孩子节点的值,
- 那这个根节点就要进行下沉操作了,
- 也就是每次下沉的时候都要去和它的两个孩子节点做比较,
- 选择它的两个孩子中最大的那个元素,
- 如果这两个孩子元素最大的那个元素比它自己还要大的话,
- 那么它自己就和两个孩子中最大的那个元素交换一下位置,
- 交换过位置之后如果还是不符合堆的性质,
- 那么就继续下沉,继续交换,直到符合堆的性质为止,
- 因为那样就不需要再下沉了,这时候你需要手动的终止,
- 如果你不手动的终止,虽然整个操作到最后也会结束,
- 但是自动终止,时间复杂度一直都是最坏的情况
O(logn)
, - 其实手动终止,最好的情况是小于
O(logn)
的, - 所以如果可以的话尽量手动终止一下。
- 堆排序的基本原理
- 大概就是 Extract 这个过程,
- 但是真正的堆排序还是有优化的空间的,
- 现在的方式是将数据扔进一个堆,
- 然后再从堆中一个一个取出来放入一个数组内,
- 还使用了额外的空间,
- 但是使用堆这种组织元素的思想完全可以将数据进行原地的排序。
- 在一个完全二叉树中,如果一个节点没有左孩子节点必然就没有右孩子节点。
代码示例
-
(class: Myarray, class: MaxHeap, class: Main)
-
Myarray
// 自定义类 class MyArray { // 构造函数,传入数组的容量capacity构造Array 默认数组的容量capacity=10 constructor(capacity = 10) { this.data = new Array(capacity); this.size = ; } // 获取数组中的元素实际个数 getSize() { return this.size; } // 获取数组的容量 getCapacity() { return this.data.length; } // 判断数组是否为空 isEmpty() { return this.size === ; } // 给数组扩容 resize(capacity) { let newArray = new Array(capacity); for (var i = ; i < this.size; i++) { newArray[i] = this.data[i]; } // let index = this.size - 1; // while (index > -1) { // newArray[index] = this.data[index]; // index --; // } this.data = newArray; } // 在指定索引处插入元素 insert(index, element) { // 先判断数组是否已满 if (this.size == this.getCapacity()) { // throw new Error("add error. Array is full."); this.resize(this.size * ); } // 然后判断索引是否符合要求 if (index < || index > this.size) { throw new Error( 'insert error. require index < 0 or index > size.' ); } // 最后 将指定索引处腾出来 // 从指定索引处开始,所有数组元素全部往后移动一位 // 从后往前移动 for (let i = this.size - ; i >= index; i--) { this.data[i + ] = this.data[i]; } // 在指定索引处插入元素 this.data[index] = element; // 维护一下size this.size++; } // 扩展 在数组最前面插入一个元素 unshift(element) { this.insert(, element); } // 扩展 在数组最后面插入一个元素 push(element) { this.insert(this.size, element); } // 其实在数组中添加元素 就相当于在数组最后面插入一个元素 add(element) { if (this.size == this.getCapacity()) { // throw new Error("add error. Array is full."); this.resize(this.size * ); } // size其实指向的是 当前数组最后一个元素的 后一个位置的索引。 this.data[this.size] = element; // 维护size this.size++; } // get get(index) { // 不能访问没有存放元素的位置 if (index < || index >= this.size) { throw new Error('get error. index < 0 or index >= size.'); } return this.data[index]; } // 扩展: 获取数组中第一个元素 getFirst() { return this.get(); } // 扩展: 获取数组中最后一个元素 getLast() { return this.get(this.size - ); } // set set(index, newElement) { // 不能修改没有存放元素的位置 if (index < || index >= this.size) { throw new Error('set error. index < 0 or index >= size.'); } this.data[index] = newElement; } // contain contain(element) { for (var i = ; i < this.size; i++) { if (this.data[i] === element) { return true; } } return false; } // find find(element) { for (var i = ; i < this.size; i++) { if (this.data[i] === element) { return i; } } return ; } // findAll findAll(element) { // 创建一个自定义数组来存取这些 元素的索引 let myarray = new MyArray(this.size); for (var i = ; i < this.size; i++) { if (this.data[i] === element) { myarray.push(i); } } // 返回这个自定义数组 return myarray; } // 删除指定索引处的元素 remove(index) { // 索引合法性验证 if (index < || index >= this.size) { throw new Error('remove error. index < 0 or index >= size.'); } // 暂存即将要被删除的元素 let element = this.data[index]; // 后面的元素覆盖前面的元素 for (let i = index; i < this.size - ; i++) { this.data[i] = this.data[i + ]; } this.size--; this.data[this.size] = null; // 如果size 为容量的四分之一时 就可以缩容了 // 防止复杂度震荡 if (Math.floor(this.getCapacity() / ) === this.size) { // 缩容一半 this.resize(Math.floor(this.getCapacity() / )); } return element; } // 扩展:删除数组中第一个元素 shift() { return this.remove(); } // 扩展: 删除数组中最后一个元素 pop() { return this.remove(this.size - ); } // 扩展: 根据元素来进行删除 removeElement(element) { let index = this.find(element); if (index !== ) { this.remove(index); } } // 扩展: 根据元素来删除所有元素 removeAllElement(element) { let index = this.find(element); while (index != ) { this.remove(index); index = this.find(element); } // let indexArray = this.findAll(element); // let cur, index = 0; // for (var i = 0; i < indexArray.getSize(); i++) { // // 每删除一个元素 原数组中就少一个元素, // // 索引数组中的索引值是按照大小顺序排列的, // // 所以 这个cur记录的是 原数组元素索引的偏移量 // // 只有这样才能够正确的删除元素。 // index = indexArray.get(i) - cur++; // this.remove(index); // } } // 新增: 交换两个索引位置的变量 2018-11-6 swap(indexA, indexB) { if ( indexA < || indexA >= this.size || indexB < || indexB >= this.size ) throw new Error('Index is Illegal.'); // 索引越界异常 let temp = this.data[indexA]; this.data[indexA] = this.data[indexB]; this.data[indexB] = temp; } // @Override toString 2018-10-17-jwl toString() { let arrInfo = `Array: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`; arrInfo += `data = [`; for (var i = ; i < this.size - ; i++) { arrInfo += `${this.data[i]}, `; } if (!this.isEmpty()) { arrInfo += `${this.data[this.size - ]}`; } arrInfo += `]`; // 在页面上展示 document.body.innerHTML += `${arrInfo}<br /><br /> `; return arrInfo; } } 复制代码
-
MaxHeap
// 自定义二叉堆之最大堆 class MyMaxHeap { constructor(capacity = 10) { this.myArray = new MyArray(capacity); } // 添加操作 add(element) { // 追加元素 this.myArray.push(element); // 将追加的元素上浮到堆中合适的位置 this.siftUp(this.myArray.getSize() - ); } // 堆的上浮操作 - siftUp(index) { // this.nonRecursiveSiftUp(index); this.recursiveSiftUp(index); // 无论是递归还是非递归都有一个 // 元素上浮后结束的条件 当前节点元素值 小于其父节点元素值 // 和 // 索引即将越界的终止条件 要上浮的元素索引 小于等于0 } // 堆的上浮操作 递归算法 - recursiveSiftUp(index) { // 解决最基本的问题, 递归终止条件 if (index <= ) return; let currentValue = this.myArray.get(index); let parentIndex = this.calcParentIndex(index); let parentValue = this.myArray.get(parentIndex); // 递归写法 if (this.compare(currentValue, parentValue) > ) { this.swap(index, parentIndex); this.recursiveSiftUp(parentIndex); } } // 堆的上浮操作 非递归算法 - nonRecursiveSiftUp(index) { if (index <= ) return; let currentValue = this.myArray.get(index); let parentIndex = this.calcParentIndex(index); let parentValue = this.myArray.get(parentIndex); while (this.compare(currentValue, parentValue) > ) { // 交换堆中两个元素位置的值 this.swap(index, parentIndex); // 交换了位置之后,元素上浮后的索引变量也要进行相应的变更 index = parentIndex; // 如果索引小于等于0了 那就结束循环 if (index <= ) break; currentValue = this.myArray.get(index); parentIndex = this.calcParentIndex(index); parentValue = this.myArray.get(parentIndex); } } // 找到优先级最大的元素 (查找元素)操作 findMax() { if (this.myArray.isEmpty()) throw new Error('can not findMax when heap is empty.'); return this.myArray.getFirst(); } // 提取优先级最大的元素(删除元素)操作 extractMax() { // 获取堆顶的元素 let maxElement = this.findMax(); // 获取堆底的元素 let element = this.myArray.getLast(); // 让堆底的元素替换掉堆顶的元素 this.myArray.set(, element); // 移除堆底的元素 this.myArray.pop(); // 让堆顶的元素开始下沉,从而能够正常满足堆的性质 this.siftDown(); // 返回堆顶的元素 return maxElement; } // 堆的下沉操作 - siftDown(index) { // this.nonRecursiveSiftDown(index); this.recursiveSiftDown(index); } // 堆的下沉操作 递归算法 - recursiveSiftDown(index) { // 递归终止条件 // 如果当前索引位置的元素没有左孩子就说也没有右孩子, // 那么可以直接终止,因为无法下沉 if (this.calcLeftChildIndex(index) >= this.myArray.getSize()) return; const leftChildIndex = this.calcLeftChildIndex(index); const leftChildValue = this.myArray.get(leftChildIndex); const rightChildIndex = this.calcRightChildIndex(index); let rightChildValue = null; // let maxIndex = 0; // if (rightChildIndex >= this.myArray.getSize()) // maxIndex = leftChildIndex; // else { // rightChildValue = this.myArray.get(rightChildIndex); // if (this.compare(leftChildValue, rightChildValue) > 0) // maxIndex = leftChildIndex; // else // maxIndex = rightChildIndex; // } // 这段代码是上面注释代码的优化 let maxIndex = leftChildIndex; if (rightChildIndex < this.myArray.getSize()) { rightChildValue = this.myArray.get(rightChildIndex); if (this.compare(leftChildValue, rightChildValue) < ) maxIndex = rightChildIndex; } let maxValue = this.myArray.get(maxIndex); let currentValue = this.myArray.get(index); if (this.compare(maxValue, currentValue) > ) { // 交换位置 this.swap(maxIndex, index); // 继续下沉 this.recursiveSiftDown(maxIndex); } } // 堆的下沉操作 非递归算法 - nonRecursiveSiftDown(index) { // 该索引位置的元素有左右孩子节点才可以下沉, // 在完全二叉树中 如果一个节点没有左孩子必然没有右孩子 while (this.calcLeftChildIndex(index) < this.myArray.getSize()) { let leftChildIndex = this.calcLeftChildIndex(index); let leftChildValue = this.myArray.get(leftChildIndex); let rightChildIndex = this.calcRightChildIndex(index); let rightChildValue = null; let maxIndex = leftChildIndex; if (rightChildIndex < this.myArray.getSize()) { rightChildValue = this.myArray.get(rightChildIndex); if (this.compare(leftChildValue, rightChildValue) <= ) maxIndex = rightChildIndex; } let maxValue = this.myArray.get(maxIndex); let currentValue = this.myArray.get(index); if (this.compare(maxValue, currentValue) > ) { this.swap(maxIndex, index); index = maxIndex; continue; } else break; } } // 堆中两个元素的位置进行交换 swap(indexA, indexB) { this.myArray.swap(indexA, indexB); } // 辅助函数 计算出堆中指定索引位置的元素其父节点的索引 - calcParentIndex(index) { if (index === ) // 索引为0是根节点,根节点没有父亲节点,小于0就更加不可以了 throw new Error("index is 0. doesn't have parent."); return Math.floor((index - ) / ); } // 辅助函数 计算出堆中指定索引位置的元素其左孩子节点的索引 - calcLeftChildIndex(index) { return index * + ; } // 辅助函数 计算出堆中指定索引位置的元素其右孩子节点的索引 - calcRightChildIndex(index) { return index * + ; } // 比较的功能 - compare(elementA, elementB) { if (elementA === null || elementB === null) throw new Error("element is error. element can't compare."); if (elementA > elementB) return ; else if (elementA < elementB) return ; else return ; } // 获取堆中实际的元素个数 size() { return this.myArray.getSize(); } // 返回堆中元素是否为空的判断值 isEmpty() { return this.myArray.isEmpty(); } } 复制代码
-
Main
// main 函数 class Main { constructor() { this.alterLine('MyMaxHeap Area'); const n = ; const maxHeap = new MyMaxHeap(); const random = Math.random; // 循环添加随机数的值 for (let i = ; i < n; i++) maxHeap.add(random() * n); console.log('MaxHeap maxHeap size:' + maxHeap.size()); this.show('MaxHeap maxHeap size:' + maxHeap.size()); // 使用数组取值 let arr = []; for (let i = ; i < n; i++) arr[i] = maxHeap.extractMax(); console.log( 'Array arr size:' + arr.length + ',MaxHeap maxHeap size:' + maxHeap.size() ); this.show( 'Array arr size:' + arr.length + ',MaxHeap maxHeap size:' + maxHeap.size() ); console.log(arr, maxHeap); // 检验一下是否符合要求 for (let i = ; i < n; i++) if (arr[i - ] < arr[i]) throw new Error('error.'); console.log('test maxHeap completed.'); this.show('test maxHeap completed.'); } // 将内容显示在页面上 show(content) { document.body.innerHTML += `${content}<br /><br />`; } // 展示分割线 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}<br /><br />`; } } // 页面加载完毕 window.onload = function() { // 执行主函数 new Main(); }; 复制代码
堆的时间复杂度分析
- 堆中的时间复杂度都是
O(logn)
级别的- 其实还是二叉树的高度这个级别的,
- 对于堆来说它是一棵完全二叉树,
- 所以它永远不会退化成一个链表,
- 一棵完全二叉树它的高度和节点的数量之间的关系一定是
logn
这个级别的关系, - 这使得堆中相应的 add、extractMax 操作是非常的高效的。
- add
O(logn)
- extractMax
O(logn)
- 可以给堆再添加两个操作,从而对这个堆再进行优化。