【从蛋壳到满天飞】JS 数据结构解析和算法实现-堆和优先队列(二)
前言
【从蛋壳到满天飞】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,点击我吧,光看文章能够掌握两成,动手敲代码、动脑思考、画图才可以掌握八成。
本文章适合 对数据结构想了解并且感兴趣的人群,文章风格一如既往如此,就觉得手机上看起来比较方便,这样显得比较有条理,整理这些笔记加源码,时间跨度也算将近半年时间了,希望对想学习数据结构的人或者正在学习数据结构的人群有帮助。
堆的另外两个操作,Heapify 和 replace
- 从理论上讲这两个操作
- 可以使用之前堆中的操作来组合实现出来,
- 但是这两个操作是比较常用的,
- 而且可以通过对他们内部的实现来进行优化,
- 所以不要使用组合原操作的方式来实现它。
replace
- 取出堆中最大元素之后,再放入一个新的元素。
- 这个过程相当于是,堆中元素总数是没有变化的,
- 这样的一个操作其实就是 extractMax 和 add,
- 但是这会导致两次的
O(logn)
级别的操作。 - 由于整个过程中堆中的元素个数并没有发生改变,
- 那么可以直接堆顶的元素替换成新的元素,
- 替换成新的元素之后,这个新的元素有可能违背了堆的性质,
- 那么直接进行 Sift Down 操作就可以了,
- 那么就只是一次
O(logn)
级别的操作。
heapify
- 将任意的一个数组整理成堆的形状
- 由于堆是一棵完全二叉树,所以它可以直接用一个数组来表示,
- 所以只要合理的交换数组中元素的位置,也可以将它整理成堆的形状,
- 你可以先扫描一下当前的数组,
- 将当前数组中所有的元素添加进这个堆所对应的对象中,
- 其实也就完成了这个工作,
- 但是还有一个更加快速的方式,这个过程就叫做 Heapify,
- 首先将当前数组看成一棵完全二叉树,
- 虽然它目标并不太可能符合堆的性质,
- 但是 对于这个完全二叉树,
- 可以从最后一个非叶子节点开始进行 Sift Down 这个操作,
- 也就是不断的进行下沉操作就可以了,
- 从后往前不断的循环进行下沉操作,
- 直到所有非叶子节点都符合堆的性质那就 ok 了。
- 定位最后一个非叶子节点所处的数组索引
- 这是一个很经典的面试题目,在计算机考试中也会有,
- 也就是用数组来表示一棵完全二叉树,
- 那么它最后一个或者倒数第一个非叶子节点所对应的索引是多少,
- 这个问题其实非常简单,只需要拿到最后一个节点的索引,
- 然后使用这个索引计算出它的父亲节点,
- 如果起始索引是从 0 开始的,
- 那就是这个最后一个节点的索引减去一之后再除以二取整即可,
- 如果起始索引是从 1 开始的,
- 那么就是这个最后一个节点的索引直接除以二再取整就好了。
- heapify 原理
- 从倒数第一个非叶子节点向前一直遍历,
- 遍历出到了第一个非叶子节点(根节点),
- 也就是对每一个节点都进行了下沉操作,
- 然后整棵树就变成了最大堆,
- 这样一来就将一个普通的数组整理成了一个最大堆的形式,
- 从一开始就抛弃了所有的叶子节点,
- 那么几乎就抛弃了这棵二叉树中近一半的节点,
- 剩下的一半的节点进行了 Sift Down 的操作,
- 这比原来直接从一个空堆开始,一个一个添加元素的速度要快一点,
- 因为每添加一个元素都会执行一次
O(logn)
级别的操作。
Heapify 的 算法复杂度
- 将 n 个元素逐个插入到一个空堆中,算法复杂度是 O(nlogn)级别。
- 如果使用 heapify 的过程,算法复杂度就为 O(n)级别的。
- 同样把一个数组整理成堆的过程要比以逐个插入到空堆中要快一些。
- 其实使用 heapify 与不使用 heapify 是有一个质的提升,
- 这个提升是
O(n)
与O(nlogn)
的区别。
- 上浮操作 Sift Up 与下沉操作 Sift Down
- 上浮是当前节点值与其父节点进行对比,如果当前节点大于其父节点就进行位置的交换。
- 下沉是当前节点值与其左右两孩子节点中最大的值的节点进行对比,
- 如果当前节点值比左右孩子节点最大值的那个节点值小,
- 那么当前节点就和那个最大值孩子节点交换位置。
代码示例
(class: Myarray, class: MaxHeap, class: PerformanceTest, class: Main)
Myarray
// 自定义类 class MyArray {// 构造函数,传入数组的容量capacity构造Array 默认数组的容量capacity=10constructor(capacity = 10) {this.data = new Array(capacity);this.size = 0;}// 获取数组中的元素实际个数getSize() {return this.size;}// 获取数组的容量getCapacity() {return this.data.length;}// 判断数组是否为空isEmpty() {return this.size === 0;}// 给数组扩容resize(capacity) {let newArray = new Array(capacity);for (var i = 0; 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 * 2);}// 然后判断索引是否符合要求if (index < 0 || index > this.size) {throw new Error('insert error. require index < 0 or index > size.');}// 最后 将指定索引处腾出来// 从指定索引处开始,所有数组元素全部往后移动一位// 从后往前移动for (let i = this.size - 1; i >= index; i--) {this.data[i + 1] = this.data[i];}// 在指定索引处插入元素this.data[index] = element;// 维护一下sizethis.size++;}// 扩展 在数组最前面插入一个元素unshift(element) {this.insert(0, 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 * 2);}// size其实指向的是 当前数组最后一个元素的 后一个位置的索引。this.data[this.size] = element;// 维护sizethis.size++;}// getget(index) {// 不能访问没有存放元素的位置if (index < 0 || index >= this.size) {throw new Error('get error. index < 0 or index >= size.');}return this.data[index];}// 扩展: 获取数组中第一个元素getFirst() {return this.get(0);}// 扩展: 获取数组中最后一个元素getLast() {return this.get(this.size - 1);}// setset(index, newElement) {// 不能修改没有存放元素的位置if (index < 0 || index >= this.size) {throw new Error('set error. index < 0 or index >= size.');}this.data[index] = newElement;}// containcontain(element) {for (var i = 0; i < this.size; i++) {if (this.data[i] === element) {return true;}}return false;}// findfind(element) {for (var i = 0; i < this.size; i++) {if (this.data[i] === element) {return i;}}return -1;}// findAllfindAll(element) {// 创建一个自定义数组来存取这些 元素的索引let myarray = new MyArray(this.size);for (var i = 0; i < this.size; i++) {if (this.data[i] === element) {myarray.push(i);}}// 返回这个自定义数组return myarray;}// 删除指定索引处的元素remove(index) {// 索引合法性验证if (index < 0 || 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 - 1; i++) {this.data[i] = this.data[i + 1];}this.size--;this.data[this.size] = null;// 如果size 为容量的四分之一时 就可以缩容了// 防止复杂度震荡if (Math.floor(this.getCapacity() / 4) === this.size) {// 缩容一半this.resize(Math.floor(this.getCapacity() / 2));}return element;}// 扩展:删除数组中第一个元素shift() {return this.remove(0);}// 扩展: 删除数组中最后一个元素pop() {return this.remove(this.size - 1);}// 扩展: 根据元素来进行删除removeElement(element) {let index = this.find(element);if (index !== -1) {this.remove(index);}}// 扩展: 根据元素来删除所有元素removeAllElement(element) {let index = this.find(element);while (index != -1) {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-6swap(indexA, indexB) {if (indexA < 0 ||indexA >= this.size ||indexB < 0 ||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-jwltoString() {let arrInfo = `Array: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`;arrInfo += `data = [`;for (var i = 0; i < this.size - 1; i++) {arrInfo += `${this.data[i]}, `;}if (!this.isEmpty()) {arrInfo += `${this.data[this.size - 1]}`;}arrInfo += `]`;// 在页面上展示document.body.innerHTML += `${arrInfo}<br /><br /> `;return arrInfo;} } 复制代码
MaxHeap
// 自定义二叉堆之最大堆 Heap class MyMaxHeap {constructor(capacity = 10) {this.myArray = new MyArray(capacity);}// 添加操作add(element) {// 追加元素this.myArray.push(element);// 将追加的元素上浮到堆中合适的位置this.siftUp(this.myArray.getSize() - 1);}// 堆的上浮操作 -siftUp(index) {this.nonRecursiveSiftUp(index);// this.recursiveSiftUp(index);// 无论是递归还是非递归都有一个// 元素上浮后结束的条件 当前节点元素值 小于其父节点元素值// 和// 索引即将越界的终止条件 要上浮的元素索引 小于等于0}// 堆的上浮操作 递归算法 -recursiveSiftUp(index) {// 解决最基本的问题, 递归终止条件if (index <= 0) return;let currentValue = this.myArray.get(index);let parentIndex = this.calcParentIndex(index);let parentValue = this.myArray.get(parentIndex);// 递归写法if (this.compare(currentValue, parentValue) > 0) {this.swap(index, parentIndex);this.recursiveSiftUp(parentIndex);}}// 堆的上浮操作 非递归算法 -nonRecursiveSiftUp(index) {if (index <= 0) return;let currentValue = this.myArray.get(index);let parentIndex = this.calcParentIndex(index);let parentValue = this.myArray.get(parentIndex);while (this.compare(currentValue, parentValue) > 0) {// 交换堆中两个元素位置的值this.swap(index, parentIndex);// 交换了位置之后,元素上浮后的索引变量也要进行相应的变更index = parentIndex;// 如果索引小于等于0了 那就结束循环if (index <= 0) 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(0, element);// 移除堆底的元素this.myArray.pop();// 让堆顶的元素开始下沉,从而能够正常满足堆的性质this.siftDown(0);// 返回堆顶的元素return maxElement;}// 堆的下沉操作 -siftDown(index) {this.nonRecursiveSiftDown(index);// this.recursiveSiftDown(index);}// 堆的下沉操作 递归算法recursiveSiftDown(index) {// 递归终止条件// 如果当前索引位置的元素没有左孩子就说也没有右孩子,// 那么可以直接终止,因为无法下沉if (this.calcLeftChildIndex(index) >= this.myArray.getSize()) return;let leftChildIndex = this.calcLeftChildIndex(index);let leftChildValue = this.myArray.get(leftChildIndex);let 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(rightChildValue, leftChildValue) > 0)// maxIndex = rightChildIndex;// else// maxIndex = leftChildIndex;// }// 这段代码是上面注释代码的优化let maxIndex = leftChildIndex;if (rightChildIndex < this.myArray.getSize()) {rightChildValue = this.myArray.get(rightChildIndex);if (this.compare(leftChildValue, rightChildValue) < 0)maxIndex = rightChildIndex;}let maxValue = this.myArray.get(maxIndex);let currentValue = this.myArray.get(index);if (this.compare(maxValue, currentValue) > 0) {// 交换位置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) < 0)maxIndex = rightChildIndex;}let maxValue = this.myArray.get(maxIndex);let currentValue = this.myArray.get(index);if (this.compare(maxValue, currentValue) > 0) {this.swap(maxIndex, index);index = maxIndex;continue;} else break;}}// 将堆顶的元素用一个新元素替换出来replace(element) {let maxElement = this.findMax();this.myArray.set(0, element);this.siftDown(0);return maxElement;}// 将一个数组变成一个最大堆 -heapify(array) {// 将数组中的元素添加到自定义动态数组里for (const element of array) this.myArray.push(element);// 减少一个O(n)的操作,不然性能相对来说会差一些// this.myArray.data = array;// this.myArray.size = array.length;// 这个动态数组满足了一棵完全二叉树的性质// 获取 这棵完全二叉树 最后一个非叶子节点的索引let index = this.calcParentIndex(this.myArray.getSize() - 1);// 从最后一个非叶子节点开始遍历 从后向前遍历 不停的下沉, 这个就是heapify的过程// for (let i = index; i >= 0; i --) { this.siftDown(i);}while (0 <= index) this.siftDown(index--);}// 堆中两个元素的位置进行交换swap(indexA, indexB) {this.myArray.swap(indexA, indexB);}// 辅助函数 计算出堆中指定索引位置的元素其父节点的索引 -calcParentIndex(index) {if (index === 0)// 索引为0是根节点,根节点没有父亲节点,小于0就更加不可以了throw new Error("index is 0. doesn't have parent.");return Math.floor((index - 1) / 2);}// 辅助函数 计算出堆中指定索引位置的元素其左孩子节点的索引 -calcLeftChildIndex(index) {return index * 2 + 1;}// 辅助函数 计算出堆中指定索引位置的元素其右孩子节点的索引 -calcRightChildIndex(index) {return index * 2 + 2;}// 比较的功能 -compare(elementA, elementB) {if (elementA === null || elementB === null)throw new Error("element is error. element can't compare.");if (elementA > elementB) return 1;else if (elementA < elementB) return -1;else return 0;}// 获取堆中实际的元素个数size() {return this.myArray.getSize();}// 返回堆中元素是否为空的判断值isEmpty() {return this.myArray.isEmpty();} } 复制代码
PerformanceTest
// 性能测试 class PerformanceTest {constructor() {}// 对比队列testQueue(queue, openCount) {let startTime = Date.now();let random = Math.random;for (var i = 0; i < openCount; i++) {queue.enqueue(random() * openCount);}while (!queue.isEmpty()) {queue.dequeue();}let endTime = Date.now();return this.calcTime(endTime - startTime);}// 对比栈testStack(stack, openCount) {let startTime = Date.now();let random = Math.random;for (var i = 0; i < openCount; i++) {stack.push(random() * openCount);}while (!stack.isEmpty()) {stack.pop();}let endTime = Date.now();return this.calcTime(endTime - startTime);}// 对比集合testSet(set, openCount) {let startTime = Date.now();let random = Math.random;let arr = [];let temp = null;// 第一遍测试for (var i = 0; i < openCount; i++) {temp = random();// 添加重复元素,从而测试集合去重的能力set.add(temp * openCount);set.add(temp * openCount);arr.push(temp * openCount);}for (var i = 0; i < openCount; i++) {set.remove(arr[i]);}// 第二遍测试for (var i = 0; i < openCount; i++) {set.add(arr[i]);set.add(arr[i]);}while (!set.isEmpty()) {set.remove(arr[set.getSize() - 1]);}let endTime = Date.now();// 求出两次测试的平均时间let avgTime = Math.ceil((endTime - startTime) / 2);return this.calcTime(avgTime);}// 对比映射testMap(map, openCount) {let startTime = Date.now();let array = new MyArray();let random = Math.random;let temp = null;let result = null;for (var i = 0; i < openCount; i++) {temp = random();result = openCount * temp;array.add(result);array.add(result);array.add(result);array.add(result);}for (var i = 0; i < array.getSize(); i++) {result = array.get(i);if (map.contains(result)) map.add(result, map.get(result) + 1);else map.add(result, 1);}for (var i = 0; i < array.getSize(); i++) {result = array.get(i);map.remove(result);}let endTime = Date.now();return this.calcTime(endTime - startTime);}// 对比堆 主要对比 使用heapify 与 不使用heapify时的性能testHeap(heap, array, isHeapify) {const startTime = Date.now();// 是否支持 heapifyif (isHeapify) heap.heapify(array);else {for (const element of array) heap.add(element);}console.log('heap size:' + heap.size() + '\r\n');document.body.innerHTML += 'heap size:' + heap.size() + '<br /><br />';// 使用数组取值let arr = new Array(heap.size());for (let i = 0; i < arr.length; i++) arr[i] = heap.extractMax();console.log('Array size:' + arr.length + ',heap size:' + heap.size() + '\r\n');document.body.innerHTML +='Array size:' +arr.length +',heap size:' +heap.size() +'<br /><br />';// 检验一下是否符合要求for (let i = 1; i < arr.length; i++)if (arr[i - 1] < arr[i]) throw new Error('error.');console.log('test heap completed.' + '\r\n');document.body.innerHTML += 'test heap completed.' + '<br /><br />';const endTime = Date.now();return this.calcTime(endTime - startTime);}// 计算运行的时间,转换为 天-小时-分钟-秒-毫秒calcTime(result) {//获取距离的天数var day = Math.floor(result / (24 * 60 * 60 * 1000));//获取距离的小时数var hours = Math.floor((result / (60 * 60 * 1000)) % 24);//获取距离的分钟数var minutes = Math.floor((result / (60 * 1000)) % 60);//获取距离的秒数var seconds = Math.floor((result / 1000) % 60);//获取距离的毫秒数var milliSeconds = Math.floor(result % 1000);// 计算时间day = day < 10 ? '0' + day : day;hours = hours < 10 ? '0' + hours : hours;minutes = minutes < 10 ? '0' + minutes : minutes;seconds = seconds < 10 ? '0' + seconds : seconds;milliSeconds =milliSeconds < 100? milliSeconds < 10? '00' + milliSeconds: '0' + milliSeconds: milliSeconds;// 输出耗时字符串result =day +'天' +hours +'小时' +minutes +'分' +seconds +'秒' +milliSeconds +'毫秒' +' <<<<============>>>> 总毫秒数:' +result;return result;} } 复制代码
Main
// main 函数 class Main {constructor() {this.alterLine('MaxHeap Comparison Area');const n = 1000000;const maxHeapIsHeapify = new MyMaxHeap();const maxHeapNotHeapify = new MyMaxHeap();let performanceTest1 = new PerformanceTest();const random = Math.random;let arr = [];let arr1 = [];// 循环添加随机数的值for (let i = 0; i < n; i++) {arr.push(random() * n);arr1.push(arr[i]);}this.alterLine('MaxHeap Is Heapify Area');const maxHeapIsHeapifyInfo = performanceTest1.testHeap(maxHeapIsHeapify,arr,true);console.log(maxHeapIsHeapifyInfo);this.show(maxHeapIsHeapifyInfo);this.alterLine('MaxHeap Not Heapify Area');const maxHeapNotHeapifyInfo = performanceTest1.testHeap(maxHeapNotHeapify,arr1,false);console.log(maxHeapNotHeapifyInfo);this.show(maxHeapNotHeapifyInfo);// this.alterLine("MyMaxHeap Replace Area");// const n = 20;// const maxHeap = new MyMaxHeap();// const random = Math.random;// // 循环添加随机数的值// for (let i = 0; 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 = 0; i < n ; i++)// arr[i] = maxHeap.replace(0);// 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 = 1; i < n; i++)// if (arr[i - 1] < 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(1)
的复杂度, - 但是出队的那个操作,寻找那个最大值就是
O(n)
的复杂度, - 如果使用顺序的线性结构的时候,那会有点相反,
- 入队会变成
O(n)
的复杂度,因为要找到待插入的这个元素的正确位置, - 出队会是
O(1)
的复杂度。
代码示例
(class: Myarray, class: MaxHeap, class: MyPriorityQueue)
Myarray
// 自定义类 class MyArray {// 构造函数,传入数组的容量capacity构造Array 默认数组的容量capacity=10constructor(capacity = 10) {this.data = new Array(capacity);this.size = 0;}// 获取数组中的元素实际个数getSize() {return this.size;}// 获取数组的容量getCapacity() {return this.data.length;}// 判断数组是否为空isEmpty() {return this.size === 0;}// 给数组扩容resize(capacity) {let newArray = new Array(capacity);for (var i = 0; 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 * 2);}// 然后判断索引是否符合要求if (index < 0 || index > this.size) {throw new Error('insert error. require index < 0 or index > size.');}// 最后 将指定索引处腾出来// 从指定索引处开始,所有数组元素全部往后移动一位// 从后往前移动for (let i = this.size - 1; i >= index; i--) {this.data[i + 1] = this.data[i];}// 在指定索引处插入元素this.data[index] = element;// 维护一下sizethis.size++;}// 扩展 在数组最前面插入一个元素unshift(element) {this.insert(0, 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 * 2);}// size其实指向的是 当前数组最后一个元素的 后一个位置的索引。this.data[this.size] = element;// 维护sizethis.size++;}// getget(index) {// 不能访问没有存放元素的位置if (index < 0 || index >= this.size) {throw new Error('get error. index < 0 or index >= size.');}return this.data[index];}// 扩展: 获取数组中第一个元素getFirst() {return this.get(0);}// 扩展: 获取数组中最后一个元素getLast() {return this.get(this.size - 1);}// setset(index, newElement) {// 不能修改没有存放元素的位置if (index < 0 || index >= this.size) {throw new Error('set error. index < 0 or index >= size.');}this.data[index] = newElement;}// containcontain(element) {for (var i = 0; i < this.size; i++) {if (this.data[i] === element) {return true;}}return false;}// findfind(element) {for (var i = 0; i < this.size; i++) {if (this.data[i] === element) {return i;}}return -1;}// findAllfindAll(element) {// 创建一个自定义数组来存取这些 元素的索引let myarray = new MyArray(this.size);for (var i = 0; i < this.size; i++) {if (this.data[i] === element) {myarray.push(i);}}// 返回这个自定义数组return myarray;}// 删除指定索引处的元素remove(index) {// 索引合法性验证if (index < 0 || 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 - 1; i++) {this.data[i] = this.data[i + 1];}this.size--;this.data[this.size] = null;// 如果size 为容量的四分之一时 就可以缩容了// 防止复杂度震荡if (Math.floor(this.getCapacity() / 4) === this.size) {// 缩容一半this.resize(Math.floor(this.getCapacity() / 2));}return element;}// 扩展:删除数组中第一个元素shift() {return this.remove(0);}// 扩展: 删除数组中最后一个元素pop() {return this.remove(this.size - 1);}// 扩展: 根据元素来进行删除removeElement(element) {let index = this.find(element);if (index !== -1) {this.remove(index);}}// 扩展: 根据元素来删除所有元素removeAllElement(element) {let index = this.find(element);while (index != -1) {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-6swap(indexA, indexB) {if (indexA < 0 ||indexA >= this.size ||indexB < 0 ||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-jwltoString() {let arrInfo = `Array: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`;arrInfo += `data = [`;for (var i = 0; i < this.size - 1; i++) {arrInfo += `${this.data[i]}, `;}if (!this.isEmpty()) {arrInfo += `${this.data[this.size - 1]}`;}arrInfo += `]`;// 在页面上展示document.body.innerHTML += `${arrInfo}<br /><br /> `;return arrInfo;} } 复制代码
MaxHeap
// 自定义二叉堆之最大堆 Heap class MyMaxHeap {constructor(capacity = 10) {this.myArray = new MyArray(capacity);}// 添加操作add(element) {// 追加元素this.myArray.push(element);// 将追加的元素上浮到堆中合适的位置this.siftUp(this.myArray.getSize() - 1);}// 堆的上浮操作 -siftUp(index) {this.nonRecursiveSiftUp(index);// this.recursiveSiftUp(index);// 无论是递归还是非递归都有一个// 元素上浮后结束的条件 当前节点元素值 小于其父节点元素值// 和// 索引即将越界的终止条件 要上浮的元素索引 小于等于0}// 堆的上浮操作 递归算法 -recursiveSiftUp(index) {// 解决最基本的问题, 递归终止条件if (index <= 0) return;let currentValue = this.myArray.get(index);let parentIndex = this.calcParentIndex(index);let parentValue = this.myArray.get(parentIndex);// 递归写法if (this.compare(currentValue, parentValue) > 0) {this.swap(index, parentIndex);this.recursiveSiftUp(parentIndex);}}// 堆的上浮操作 非递归算法 -nonRecursiveSiftUp(index) {if (index <= 0) return;let currentValue = this.myArray.get(index);let parentIndex = this.calcParentIndex(index);let parentValue = this.myArray.get(parentIndex);while (this.compare(currentValue, parentValue) > 0) {// 交换堆中两个元素位置的值this.swap(index, parentIndex);// 交换了位置之后,元素上浮后的索引变量也要进行相应的变更index = parentIndex;// 如果索引小于等于0了 那就结束循环if (index <= 0) 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(0, element);// 移除堆底的元素this.myArray.pop();// 让堆顶的元素开始下沉,从而能够正常满足堆的性质this.siftDown(0);// 返回堆顶的元素return maxElement;}// 堆的下沉操作 -siftDown(index) {this.nonRecursiveSiftDown(index);// this.recursiveSiftDown(index);}// 堆的下沉操作 递归算法recursiveSiftDown(index) {// 递归终止条件// 如果当前索引位置的元素没有左孩子就说也没有右孩子,// 那么可以直接终止,因为无法下沉if (this.calcLeftChildIndex(index) >= this.myArray.getSize()) return;let leftChildIndex = this.calcLeftChildIndex(index);let leftChildValue = this.myArray.get(leftChildIndex);let 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(rightChildValue, leftChildValue) > 0)// maxIndex = rightChildIndex;// else// maxIndex = leftChildIndex;// }// 这段代码是上面注释代码的优化let maxIndex = leftChildIndex;if (rightChildIndex < this.myArray.getSize()) {rightChildValue = this.myArray.get(rightChildIndex);if (this.compare(leftChildValue, rightChildValue) < 0)maxIndex = rightChildIndex;}let maxValue = this.myArray.get(maxIndex);let currentValue = this.myArray.get(index);if (this.compare(maxValue, currentValue) > 0) {// 交换位置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) < 0)maxIndex = rightChildIndex;}let maxValue = this.myArray.get(maxIndex);let currentValue = this.myArray.get(index);if (this.compare(maxValue, currentValue) > 0) {this.swap(maxIndex, index);index = maxIndex;continue;} else break;}}// 将堆顶的元素用一个新元素替换出来replace(element) {let maxElement = this.findMax();this.myArray.set(0, element);this.siftDown(0);return maxElement;}// 将一个数组变成一个最大堆 -heapify(array) {// 将数组中的元素添加到自定义动态数组里for (const element of array) this.myArray.push(element);// 减少一个O(n)的操作,不然性能相对来说会差一些// this.myArray.data = array;// this.myArray.size = array.length;// 这个动态数组满足了一棵完全二叉树的性质// 获取 这棵完全二叉树 最后一个非叶子节点的索引let index = this.calcParentIndex(this.myArray.getSize() - 1);// 从最后一个非叶子节点开始遍历 从后向前遍历 不停的下沉, 这个就是heapify的过程// for (let i = index; i >= 0; i --) { this.siftDown(i);}while (0 <= index) this.siftDown(index--);}// 堆中两个元素的位置进行交换swap(indexA, indexB) {this.myArray.swap(indexA, indexB);}// 辅助函数 计算出堆中指定索引位置的元素其父节点的索引 -calcParentIndex(index) {if (index === 0)// 索引为0是根节点,根节点没有父亲节点,小于0就更加不可以了throw new Error("index is 0. doesn't have parent.");return Math.floor((index - 1) / 2);}// 辅助函数 计算出堆中指定索引位置的元素其左孩子节点的索引 -calcLeftChildIndex(index) {return index * 2 + 1;}// 辅助函数 计算出堆中指定索引位置的元素其右孩子节点的索引 -calcRightChildIndex(index) {return index * 2 + 2;}// 比较的功能 -compare(elementA, elementB) {if (elementA === null || elementB === null)throw new Error("element is error. element can't compare.");if (elementA > elementB) return 1;else if (elementA < elementB) return -1;else return 0;}// 获取堆中实际的元素个数size() {return this.myArray.getSize();}// 返回堆中元素是否为空的判断值isEmpty() {return this.myArray.isEmpty();} } 复制代码
MyPriorityQueue
// 自定义优先队列 PriorityQueue class MyPriorityQueue {constructor() {this.maxHeap = new MyMaxHeap();}// 入队enqueue(element) {this.maxHeap.add(element);}// 出队dequeue() {return this.maxHeap.extractMax();}// 查看队首元素getFront() {return this.maxHeap.findMax();}// 查看队列中实际元素的个数getSize() {return this.maxHeap.size();}// 返回队列是否为空的判断值isEmpty() {return this.maxHeap.isEmpty();}// 扩展: 修改最大堆中的比较算法updateCompare(compareMethod) {// 传入参数可以替换掉原堆中实现的compare方法this.maxHeap.compare = compareMethod;}// 扩展: 用一个新元素去替换队首的元素,同时再次确认优先级别replaceFront(element) {// 这样就就可 不需要 出队入队操作这么麻烦了return this.maxHeap.replace(element);} } 复制代码
Leetcode 上优先队列相关的问题
优先队列的经典问题
- 在
1,000,000
个元素中选出前 100 名?- 也就是在 N 个元素中选出前 M 个元素,
- 如果 M 等于 1,那么就是在 N 个元素中就选择第一名的那个元素,
- 只需要遍历一遍就好了,非常的简单,
- 整个算法的时间复杂度是 O(n)级别的,
- 但是 M 如果不等于 1 的话,那么就有一点棘手,
- 最朴素的想法就是对这一百万个元素进行一下排序,
- 对于一百万这个级别的元素来说,使用高级的排序算法,
- 无论是归并排序也好还是快速排序也好
- 都可以在
NlogN
的时间里完成任务, - 整体来说这种时间复杂还是可以接受的,
- 排序完成后直接取出前一百名元素就好了,非常容易,
- 但是问题的关键在于有没有更好的方法,
- 在这个问题上,如果使用优先队列的话,
- 那么就可以在
NlogM
这个时间复杂度内解决问题, - 如果这个 M 等于 100 的话,logM 大概是 7 左右。
- 如果使用高级的排序算法,
- 在
NlogN
这个时间复杂度内的话, - 那么 logN 大概是 20。
- 这样一来它们相差了三倍左右,
- 所以 NlogM 是比 NlogN 更好的时间复杂度。
- 使用优先队列,维护当前看到的前 M 个元素
- 对于这 100 万个元素,肯定是要从头到尾扫描一遍,
- 在扫描的过程中,首先将这 N 个元素中的前 M 个元素放入优先队列中,
- 之后每次看到一个新的元素,
- 如果这个新的元素比当前优先队列中最小的元素还要大,
- 那么就把优先队列中最小的那个元素给扔出去,
- 取而代之的换上这个新的元素,用这样的方式,
- 相当于这个优先队列中一直维护者当前可以看到的前 M 个元素,
- 直到把这 n 个元素全都扫描完,在优先队列中最终留下来的这 M 个元素,
- 就是最终要求的结果。
- 实际需要的是一个最小堆 MinHeap,
- 要能够非常快速的取出当前看到前 M 个元素中最小的那个元素,
- 需要不断的将当前可以看到的前 M 大的元素中最小的元素进行替换,
- 已经实现了最大堆 MaxHeap,你只需要把这个逻辑改一改,
- 把它变成一个最小堆 MinHeap 是非常容易的,
- 就是将核心逻辑比较的时候符号进行一个改变。
- 实际上解决这个问题并不需要真的使用最小堆 MinHeap,
- 依然使用最大堆也是可以的,这里面最关键的怎么去定义优先级,
- 优先级的大小,没有谁规定最大的元素优先级就是最高的,
- 这个问题的解决方案中,每次都是去取出这个优先队列中最小的元素,
- 那么就完全可以自己去定义,例如元素的值越小,它的优先级越高,
- 在这样的一个定义下,依然可以使用底层以最大堆实现的优先队列了,
- 大和小其实是相对的。
在 leetcode 上的问题
347.前K个高频元素
https://leetcode-cn.com/problems/top-k-frequent-elements/
,- 可以使用 系统内置的 Map 来统计频率,
- 然后使用 PriorityQueue 来进行频率的优先级统计,
- 由于自己实现的自定义优先队列是以一个最大堆为底层实现,
- 那么入队的元素的比较操作需要相反,
- 要支持的是,频次越低优先级就越高,
- 那么当当前元素的频次越低,就让它在堆的最顶端,
- 那么 compareTo 操作时,返回的值为正数则会进行向上浮动,
- 返回的值如果为负数则会进行下沉。
代码示例
// 答题 class Solution {// leetcode 347. 前K个高频元素topKFrequent(nums, k) {/*** @param {number[]} nums* @param {number} k* @return {number[]}* 原版*/var topKFrequent = function(nums, k) {let map = new Map();// 统计 数组中每一个元素出现频率for (const num of nums) {if (map.has(num)) map.set(num, map.get(num) + 1);else map.set(num, 1);}// 优先队列:使用的时候指定优先级比较的方式let queue = new MyPriorityQueue();// 变更优先队列中的定义优先级的方法queue.updateCompare((elementA, elementB) => {// 原的比较算法是 值越大 优先级越大// 现在改为 值越小 优先级越大if (elementA.value < elementB.value) return 1;else if (elementA.value > elementB.value) return -1;else return 0;});for (const key of map.keys()) {if (queue.getSize() < k)queue.enqueue({ key: key, value: map.get(key) });else if (map.get(key) > queue.getFront().value) {queue.replaceFront({ key: key, value: map.get(key) });// queue.dequeue();// queue.enqueue({"key": key, "value": map.get(key)});}}let result = [];for (var i = 0; i < k; i++) {result.push(queue.dequeue().key);}return result;};// 精简版var topKFrequent = function(nums, k) {let map = new Map();// 统计 数组中每一个元素出现频率for (const num of nums) {if (map.has(num)) map.set(num, map.get(num) + 1);else map.set(num, 1);}// 优先队列:使用的时候指定优先级比较的方式let queue = new MyPriorityQueue();// 变更优先队列中的定义优先级的方法queue.updateCompare((keyA, keyB) => {// 原的比较算法是 值越大 优先级越大// 现在改为 值越小 优先级越大if (map.get(keyA) < map.get(keyB)) return 1;else if (map.get(keyA) > map.get(keyB)) return -1;else return 0;});for (const key of map.keys()) {if (queue.getSize() < k) queue.enqueue(key);else if (map.get(key) > map.get(queue.getFront())) {queue.replaceFront(key);}}let result = [];for (var i = 0; i < k; i++) {result.push(queue.dequeue());}return result;};return topKFrequent(nums, k);} }// main 函数 class Main {constructor() {this.alterLine('leetcode 347. 前K个高频元素');let s = new Solution();let arr = [5,-3,9,1,7,7,9,10,2,2,10,10,3,-1,3,7,-9,-1,3,3];console.log(arr);this.show(arr);let result = s.topKFrequent(arr, 3);console.log(result);this.show(result);}// 将内容显示在页面上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(); }; 复制代码
和堆相关的更多话题及广义队列
- leetcode 中与堆相关的题目
https://leetcode-cn.com/tag/heap/
- 自己实现的堆其实是二叉堆 Binary Heap
- 计算机世界中其实还有各种各样的堆,
- 学会了二叉堆之后,最容易拓展的就是 d 叉堆 d-ary heap 了,
- 也就是说,对于每一个节点来说,它可能有三个四个甚至更多个孩子,
- 也排列成完全 d 叉树这种形式,用这样的方式也可以构建出一个堆来,
- 对于这种堆而言,其实它的层数是更加的低了,
- 那么对它的添加操作删除操作,
- 相应的时间复杂度都变成了 log 以 d 为底 n 这样的时间复杂度,
- 从这个时间复杂度的角度来讲,好像比 log 以 2 为底 n 这样的时间复杂度要好,
- 可是相应的代价会越高,比如每一个节点的 SiftDown 下沉操作时,
- 需要考虑的节点数变多了,不仅仅是考虑两个节点了,而是要考虑 d 个节点,
- 它们之间就存在了一个制衡的关系。
- 自己实现的堆有一个很大的缺点
- 只能看到堆首的元素,却不能看到堆中间的元素,
- 实际上在很多应用中是需要看到堆中间的元素,
- 甚至需要对堆中间的元素进行一定的修改,
- 在这种情况下相应的就要有一个
索引堆
这样的数据结构, - 这种堆除了保持你关注的那个元素之外,还对应了一个索引,
- 可以通过这个索引非常方便的检索到元素存在堆中的哪个位置,
- 甚至可以根据索引来修改这个元素,
- 事实上
索引堆
还是应用非常广泛的一种数据结构, - 不过这种数据结构相对是比较高级的,
- 在慕课网《算法与数据结构》中第四章 8-9 节有,
- 无论是最小生成树算法还是最短路径算法,
- 也就是对于 Prim 算法和 Dijkstra 算法都可以使用索引堆进行优化,
- 在真实的面试中,几乎不会问索引堆的问题。
- 在计算机的世界中还有各种奇奇怪怪的堆
- 二项堆、斐波那契堆,这些堆更是更高级的数据结构。
广义队列
Queue
void enqueue(e)
E dequeue()
E getFront()
int getSize()
boolean isEmpty()
- 只要支持这样的接口或者支持入队或者出队操作,它就可以叫做一个队列。
- 这么定义一个队列的话,那么队列这个概念就太广义了,
- 如普通队列(先到先得)、优先队列(优先级最高的先出队),
- 如此一来,栈其实也可以理解是一个队列,
- 入栈和出栈操作也是向一个数据结构添加元素和拿出元素,
- 所以栈也可以理解成是一个队列,
- 事实上当你这么理解的时候,对于很多计算机算法,
- 你理解的角度将会产生新的变化,最典型的例子如二分搜索树,
- 实现了一个非递归的前序遍历、层序遍历,
- 这两个算法基本的逻辑是完全一样的,
- 区别只在于一个使用的栈一个使用的队列,
- 当你把栈也看做队列的时候,这两种方式非常完美的统一了,
- 对于这两种遍历方式来说,它们的区别在于你使用了怎样的数据结构,
- 而不是具体的逻辑,这个具体的逻辑其实是一致的。
【从蛋壳到满天飞】JS 数据结构解析和算法实现-堆和优先队列(二)相关推荐
- 【从蛋壳到满天飞】JS 数据结构解析和算法实现-堆和优先队列(一)
前言 [从蛋壳到满天飞]JS 数据结构解析和算法实现,全部文章大概的内容如下: Arrays(数组).Stacks(栈).Queues(队列).LinkedList(链表).Recursion(递归思 ...
- 【从蛋壳到满天飞】JS 数据结构解析和算法实现-AVL树(一)
前言 [从蛋壳到满天飞]JS 数据结构解析和算法实现,全部文章大概的内容如下: Arrays(数组).Stacks(栈).Queues(队列).LinkedList(链表).Recursion(递归思 ...
- 【从蛋壳到满天飞】JS 数据结构解析和算法实现-哈希表
前言 [从蛋壳到满天飞]JS 数据结构解析和算法实现,全部文章大概的内容如下: Arrays(数组).Stacks(栈).Queues(队列).LinkedList(链表).Recursion(递归思 ...
- 【从蛋壳到满天飞】JS 数据结构解析和算法实现-集合和映射
前言 [从蛋壳到满天飞]JS 数据结构解析和算法实现,全部文章大概的内容如下: Arrays(数组).Stacks(栈).Queues(队列).LinkedList(链表).Recursion(递归思 ...
- JS 数据结构之旅 :通过JS实现栈、队列、二叉树、二分搜索树、AVL树、Trie树、并查集树、堆
JS 数据结构之旅 栈 概念 栈是一个线性结构,在计算机中是一个相当常见的数据结构. 栈的特点是只能在某一端添加或删除数据,遵循先进后出的原则 实现 每种数据结构都可以用很多种方式来实现,其实可以把栈 ...
- Three.js数据结构、导入导出(.toJSON())
Three.js数据结构.导入导出 本文是Three.js电子书的14.1节 通过Three.js模型数据导入导出过程的学习,可以让你对Threejs解析加载外部模型的过程更为了解. Threejs导 ...
- 爬虫之JS的解析确定js的位置
爬虫之JS的解析确定js的位置 对于前面人人网的案例( http://www.renren.com),我们知道了url地址中有部分参数,但是参数是如何生成的呢? 毫无疑问,参数肯定是js生成的,那么如 ...
- FreeMarker对应各种数据结构解析
FreeMarker对应各种数据结构解析 FreeMarker 是一个采用 Java 开发的模版引擎,是一个基于模版生成文本的通用工具. FreeMarker 被设计用来生成 HTML Web 页面, ...
- JS数据结构与算法_链表
上一篇:JS数据结构与算法_栈&队列 下一篇:JS数据结构与算法_集合&字典 写在前面 说明:JS数据结构与算法 系列文章的代码和示例均可在此找到 上一篇博客发布以后,仅几天的时间竟然 ...
- js读取解析JSON类型数据【申明:来源于网络】
js读取解析JSON类型数据[申明:来源于网络] 地址:http://blog.csdn.net/sunhuaqiang1/article/details/47026841 转载于:https://w ...
最新文章
- 深入jQuery中的data()
- 编程字典keras.layers API方法
- SAP IDoc Post不成功,报错 - Conventional invoice verification no longer maintained as of Release 4.6-
- golang runtime.systemstack 泄漏排查
- 【本周面试题】第5周 - 开发工具相关
- LeetCode 116. Populating Next Right Pointers in Each Node
- python复杂网络库networkx:算法
- 20200621每日一句
- 与孩子一起学编程15章
- matlab2018a制图,MatLab 2018a 官方教程
- nginx 下配置禅道
- 静态小米官网首页仿站笔记
- MySql从入门到中级到事务
- VUE使用JS-SDK实现微信分享好友功能(通过点击控件触发)
- ✨【Code皮皮虾】一次通过99.90%,思路详解【找到需要补充粉笔的学生编号】
- 【控制】Matlab模拟汽车动力学分析系统
- 3a企业信用等级证书怎么办理
- Monkey脚本API简介
- 在python里面使用you-get批量下载哔哩哔哩视频
- 如何下载视频号的视频?微信视频号视频保存方法,不用进入手机管理文件去修改。
热门文章
- docker网络模式--资源分配叙述(1)
- Java实现P5713 【深基3.例5】洛谷团队系统
- Why use Spring
- WIN10计算机不支持3D游戏怎么办,win10电脑3d设置在哪里设置
- 计算机中丢失aclst16,Win10系统运行CAD2006提示计算机中丢失ac1st16.dll怎么办
- 协调才暴力-精英乒乓论坛
- 网站出现502 BAD GATEWAY的解决办法
- Bluecoat Web无法正常显示页面解决方案
- java基于for、while循环经典案例题(仅供参考)
- python电子病历,如何在电子病历上安装软件包