堆和堆排序

堆和优先队列

普通队列:FIFO,LILO

优先队列:出队顺序和入队顺序无关,和优先级相关。一个典型应用就是操作系统中。动态选择优先级高的任务执行

堆的实现

最典型的堆就是二叉堆,就像是一颗二叉树。这个堆的特点,下图可以看出:

这里以最大堆为例,,每一个节点都不大于其父亲节点。另外,堆必须是一颗完全二叉树,正因为此,我们可以使用数组来存储二叉堆如下图所示,给二叉堆自上而下,自左到右表上序号,

由图中节点序号,可以看出,如果某个节点的序号为k,则其左子节点的序号是2*k,右子节点的序号是2*k+1,这里,与通常我们数组的规定不同,根节点是从1开始的,不是0,这也是堆的经典实现方式。不过从0开始标定,也会有类似性质,只是常数的变化。

下面就要实现最大堆,做成一个MaxHeap 类,最大堆中要存储数据,为了通用性,将这个类做成一个模板类。这个最大堆首先得有一个存储数据的数组,在用户定义之前,我们不知道数组的大小,所以该数组是一个指针类型,相应的会在构造函数中初始化该数组。还需要一个int型的size来表示堆中元素数量。所以堆的基本框架如下:

 1 template<typename Item>
 2 class MaxHeap{
 3 private:
 4     Item* data;
 5     int count;
 6 private:
 7     int shiftDown(int k){
 8 
 9     }
10 public:
11     MaxHeap(int capacity){
12         data = new Item[capacity];
13         count=0;
14     }
15     ~MaxHeap(){
16         delete[] data;
17     }
18 
19     int size(){return count;}
20 
21     bool isEmpty(){return count==0;}
22 };

 向堆中添加、删除元素

添加元素

因为堆的性质,我们这里要用到一个添加元素时的核心操作。、;shiftUp,用下面的堆具体说一下思路

    

 

 

 

  

 

  

 因为是用数组存储的堆中的元素,最初52加入的时候如第二幅图所示,由于新元素的加入,这个堆已经不满足最大堆的性质了,我们需要根据最大堆的性质调整,给52找到正确的位置,如何调整呢?:考查52的父节点,比52小,违背了最大堆的性质,所以交换之,这样以52为根节点的子堆满足了最大堆的性质,继续考查新的堆中52的父节点,依然不满足最大堆的定义,同样的操作,继续考查,直到整个堆满足最大堆的性质。那么添加元素后只需要不断调用shiftUp操作就可以了,具体代码实现

 1 // insert(Item)
 2  2 void insert(Item item){
 3  3         data[count+1] =item;
 4  4         count++;
 5  5         shiftUp(count);
 6  6     }
 7  7 
 8  8 // shiftUp(int)
 9  9 
10 10 void shiftDown(int k){
11 11         while(data[k/2]<data[k] && k>1){
12 12             swap(data[k],data[k/2]);
13 13             k/=2;
14 14         }
15 15     }

但是, data[count+1] =item; 有一个数组越界的潜在风险,所以,我们需要在类中再定义一个capacity的成员变量,并在构造函数中使用用户指定的capacity初始化 this->capacity = capacity; 并且,在添加新元素之前assert一下,在insert(Item)函数中第三行之前加入 assert(count+1<=capacity); 当然,更好的方法就是一旦发现capacity不足够,就分配新的空间,C++primer中在讲容器的时候提到过,一般是采用倍增的方法,这里主要讲堆,我自己也没研究过,这里先Mark下,以后仔细想想具体实现,此处就先用这种简单的方法防止数组越界。

从堆中取出元素

Note: 从堆中取出元素只能取出根节点的元素。

一旦取出对顶元素,就需要调整堆,使得堆这颗二叉树依然满足堆的性质。还是以图的形式给出过程。

 

 

  

 

  

 

  

 

 

 

 由上面过程可以看出,一旦取出堆顶元素,就把堆中最后一个元素放到堆顶,不断调整,直到这颗二叉树再次满足最大堆的性质。这个不断调整的过程就是shiftDown 的过程。简单说一下这个过程。现在16处在堆顶位置,比它的左右子节点都要小,最大堆的性质要求父亲节点要大于子节点,所以,应该调整,向左还是向右是有左右的大小决定的,谁大跟谁换,这样16跟52换,然后再考查新的堆,继续考查16,直到16在它正确的位置。具体实现如下:

 1 //shiftDown(int);
 2 void shiftDown(int k){
 3         //int j = 2*k;
 4         while(2*k<=count){
 5             int j = 2*k;
 6             if(data[j]<data[j+1])
 7                 j++;
 8             if(data[k]<data[j]){
 9                 swap(data[k],data[j]);
10                 k = j;
11             }
12         }
13     }
14 
15 //get the top of heap Item top()
16 Item get(){
17         assert(count>0);
18         Item item  = data[1];
19         swap(data[1],data[count])
20         count--;
21         //swap(data[])
22         shiftDown(1);
23         return item;
24     }

前面给出了取出对顶元素的方法,如果,不断取出堆顶元素并打印出来,就是一个从大到小的数组,由此,可以想到利用堆进行排序,这个排序接收一个数组和数组元素个数,创建一个heap类的对象,通过这个对象调用insert()函数和top() 函数即可实现,具体实现

1 void heap_sort(int arr[],int n){
2     MaxHeap<int> maxHeap = MaxHeap(n);
3     for(int i = 0;i<n;i++)
4         maxHeap.insert(arr[i]);
5     for(int i = 0;i<n;i++)
6         arr[i] = maxHeap.top();
7 }

上面实现的是从大到小的排序,要想从小到大排序,反向打印就可以,只需将第5行改为 for(int i = n-1;i>=0;i--) 

堆的heapify

前面提到了最大堆排序,我将上面的堆排序实现方式与归并排序和快速排序时间做了一个比较,前面的堆排序的方式花费的时间较长,回顾一下,前面的堆排序需要将数组元素一个一个的插入堆中,利用堆不断的调整,从堆中取出元素的时候也需要不断的调整,使得二叉树依然保有最大堆的性质,这种方式的效率显然不高,说道这里,我想起来了,不断调整过程中需要不断的shiftUp和shiftDown,这两个操作都需要swap()操作,前面讲插入排序的时候提到过,swap操作相对于移动/赋值操作是低效的,所以,这里也是可以改进的,不过下面要说的是改进数组构成堆的方式,给定一个数组,我们让这个数组形成一个堆的形状,这个过程叫--Heapify.还是以图片形式演示过程,下图只给出了一部分过程的图示

 

 

 

 

 

  

 

 

 看第一幅图,右下角是一个数组,将该数组构建成一颗完全二叉树,但是,这颗二叉树不是堆,如果将这颗完全二叉树根据对的性质调整成堆,那么,就将数组构造成了堆,调整过程就是上图示过程

看第二幅图中所有叶子节点,它们本身各自就是一个堆,还是以最大堆为例,一个最大堆,Note:一颗完全二叉树,第一个非叶子节点是5,元素数目为10,可以多举些例子,可以找出规律,一颗完全二叉树第一个非叶子节点是k/2取整(k为完全二叉树节点个数),这里是上取整,并且是从1开始而不是从0开始(可以证明的)。现在自下而上考查非叶子节点,第一个是5,它的值是32,以它为根的子树不满足最大堆的性质,它比子节点小,所以做一次shiftDown操作,接着考查4位置,13也比子节点小,再执行一次shiftDown操作,接着考查位置3,也不满足,继续执行shiftDown,再看2 ,17比它的子节点小,shiftDown,但是调整到5上,依然不满足,继续shiftDown,直到17在正确的位置上, 这时,以62为根的子树满足了最大堆性质,接着向上一层,树顶元素不满足,shiftDown,直到调整到15应该在的位置。可以构建一个构造函数接收一个数组和数组容量,代码实现如下

1 MaxHeap(Item arr[],int n){
2         data = new Item[n];
3         capacity = n;
4         for(int i =0;i<n;i++)
5             data[i+1] = arr[i];
6         count = n;
7         for(int k = n/2;k>0;k--)
8             shiftDown(k);
9     }

使用这个构造函数进行heapsort

1 void heap_sort2(int arr[],int n){
2     MaxHeap<int> maxHeap = MaxHeap(arr,n);
3     for(int i = n-1;i>=0;i--)
4         arr[i] = maxHeap.top();
5 }

再次测试,时间性能上比前面的堆排序快,但依然是比归并排序和快速排序慢,不过堆排序通常用于动态数据的维护,而不是系统级别的排序。

将n个元素逐个插入到一个空堆中,时间复杂度是O(NlogN)的

但是heapify的过程,算法复杂度是O(N),这个,我没有证明。只是看书上写的

原地堆排序

前面介绍的堆排序算法都需要将数据从数组放入堆中,再从堆中取出。这需要分配额外的空间,但是,根据堆排序的思想,整个数组的排序过程可以原地进行,不需要再分配额外空间。回想之前通过数组构造堆的过程可以看出一个数组其实可以把它看成一个堆,因此可以将数组通过heapify过程构建成堆,还是以最大堆为例。 这是一个数组形式排列的最大堆,堆顶元素v为整个数组中最大的,按照排序后的结果,v应该在数组尾部,所以将v与最后一个元素w交换,交换后这个数组不再是一个最大堆(橙色部分不再是最大堆),如何调整成最大堆,根据前面的,就是不断的调用shiftDown,调整成一个最大堆(依然用暗红色表示该部分是最大堆),蓝色表示已经排序好的。

 

 

 

   

  此时又重复上面的过程,继续交换v和w,然后将未排序的部分调整成最大堆。

Note:由于整个过程在数组上原地进行,数组是从0开始索引的,所以在实现的时候注意调整,主要是shiftDown过程的索引,具体的就是父亲节点与子节点索引的关系,举个例子很容易看出来

最后一个非叶子节点同时也变成了 (count-1)/2 ,依然是上取整。

实现代码如下:

 

 1 void heapSort(int arr[],int n){
 2     //heapify
 3     // index begins with 0
 4     // the first leaf node which is not null (count-1)/2
 5     for(int i = (n-1)/2;i>=0;i--)
 6         __shiftDown(arr,n,i);
 7     for(int i = n-1;i>=0;i--){
 8         swap(arr[i],arr[0]);
 9         __shiftDown(arr,i,0);
10     }
11 }
12 
13 void __shiftDown(int arr[],int n,int k){
14     while(2*k+1<n){
15         int j = 2*k+1;
16         if(arr[j]<arr[j+1] && j+1<n)
17             j+=1;
18         if(arr[k]<arr[j]){
19             swap(arr[k],arr[j]);
20             k = j;
21         }
22     }
23 }

优化

之前提到过,用赋值操作代替交换操作会提升时间效率,在这里实现

 1 void __shiftDown2(int arr[],int n,int k){
 2     int e = arr[k];
 3     while(2*k+1<n){
 4         int j = 2*k+1;
 5         if(arr[j]<arr[j+1])
 6             j+=1;
 7         if(e>=arr[j]) break;
 8         arr[k] = arr[j];
 9         k = j;
10     }
11     arr[k] = e;
12 }

排序算法总结

到这里,所有排序算法都写完了,下面的图是我在GitHub上看到的关于排序算法的总结

这里,有一个排序算法稳定性的概念,之前没有提到过

算法稳定性:对于相等的元素在排序后,原来靠前的元素依然靠前,相等的元素的相对位置没有发生改变

我觉得他给出的这个解释的前半句非常好理解,对于算法稳定性。

 

转载于:https://www.cnblogs.com/Holly-blog/p/9321011.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/251227.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ES5-1 发展史、ECMA、编程语言、变量、JS值

1. 5大主流浏览器及内核&#xff08;自主研发&#xff09; 浏览器内核IEtridentChromewebkit blinkSafariwebkitFirefoxgeckoOperapresto 2. 浏览器的历史 和 JS诞生 1989-1991 WorldWideWeb&#xff08;后来为了避免与万维网混淆而改名为Nexus&#xff09;是世界上第一个网页…

javascript --- 使用对象关联简化整体设计

在某个场景中,我们有两个控制器对象: 1.用来操作网页中的登录表单; 2.用来与服务器进行通信. 类设计模式 // 把基础的函数定义在名为Controller的类中,然后派生两个子类LoginController和AuthController. // 父类 function Controller() {this.errors []; } Controller.prot…

javascript --- polyfill中几个常用方法

ES6中,新增了许多有用的方法,下面分享几个ES6之前得版本写的polyfill Number.EPSILON: // 机器精度,并判断2个数是否相等 if(!Number.EPSILON){Number.EPSILON math.pow(2, -52); }function numberCloseEnoughToEqual(n1, n2) {return Math.abs(n1 - n2 ) < Number.EPSIL…

[Usaco2010 Nov]Visiting Cows

题目描述 经过了几周的辛苦工作,贝茜终于迎来了一个假期.作为奶牛群中最会社交的牛,她希望去拜访N(1<N<50000)个朋友.这些朋友被标号为1..N.这些奶牛有一个不同寻常的交通系统,里面有N-1条路,每条路连接了一对编号为C1和C2的奶牛(1 < C1 < N; 1 < C2 < N; C1…

ES5-2 语法、规范、错误、运算符、判断分支、注释

1. 错误 MDN错误列表 Uncaught SyntaxError: Unexpected token ) // 语法错误 Uncaught ReferenceError: a is not defined // 引用错误等类型 Uncaught TypeError: Cannot read property toString of null出现一个语法错误&#xff0c;则一行代码都不会执行&#xff08;检查…

新单词 part 4

part 41.veto 英[ˈvi:təʊ]美[ˈvi:toʊ]n. 行使否决权; 否决权&#xff0c;否认权; 否决理由;vt. 否决&#xff0c;不同意; 不批准&#xff0c;禁止;vi. 否决; 禁止;2.acoustics 英[əˈku:stɪks]美[əˈkustɪks]n. 声学; &#xff08;传声系统的&#xff09; 音响效果; 声…

unity深度查找某个子物体和遍历所有子物体方法

本文总结一下关于unity的查找子物体的方法 首先说明一下这里将讲三种查找子物体方法&#xff1a; 查找固定路径的某一个子物体的方法、通过名字深度查找某个子物体的方法、查找父物体下所有子物体的方法。 第一:查找固定路径的某一个子物体的方法 对于已知的路径可以直接用go.t…

javascript --- JSON字符串化

工具函数JSON.stringify()将JSON对象序列化为字符串时也用到了ToString. 看下面的代码: console.log(JSON.stringify(42)); console.log(JSON.stringify("42")); console.log(JSON.stringify(null)); console.log(JSON.stringify(true));所有安全的JSON值都可以使用…

静态链接和动态链接

静态链接和动态链接 静态链接方法&#xff1a;静态链接的时候&#xff0c;载入代码就会把程序会用到的动态代码或动态代码的地址确定下来 静态库的链接可以使用静态链接&#xff0c;动态链接库也可以使用这种方法链接导入库 动态链接方法&#xff1a;使用这种方式的程序并不在一…

ES5-3 循环、引用值初始、显示及隐式类型转换

1. 循环 for循环的三个参数abc&#xff0c;a只执行一次&#xff0c;c在每次循环后执行 // 打印0-100的质数 1不是质数 var list [2] for (var i 3; i < 100; i i 2) {var flag falsefor (var j 0; j < list.length; j) {var cur list[j]if (i % cur 0 &…

hihocoder 二分

题目 一个简单的二分&#xff0c;只是想说明一下&#xff0c;如若要查找一个数组中某个数的下标可以直接用lower_bound()这个函数。只是要考虑到要查找的数不在数组中的这种情况。 #include <cstdio> #include <iostream> #include <algorithm> using namesp…

ubuntu开启ssh服务

更新资源列表&#xff1a;sudo apt-get update -> 输入管理员密码 安装openssh-server: sudo apt-get install openssh-server 查看 ssh服务是否启动&#xff1a;sudo ps -e |grep ssh 启动ssh服务&#xff1a; sudo service ssh start 转载于:https://www.cnblogs.com/ver…

javascript --- 判断只有1个为真

下面写一个用于判断只有一个为真的函数: function onlyOne(a,b,c){return !!((a && !b && !c) ||(!a && b && !c) || (!a && !b && c)); } var a true; var b false;onlyOne(a,b,b) // true onlyOne(a,b,a); // false上述…

13 代码分割之import静动态导入

前端首屏优化方案之一 项目构建时会整体打包成一个bundle的JS文件&#xff0c;而有的代码、模块是加载时不需要的&#xff0c;需要分割出来单独形成一个文件块chunk&#xff08;不会打包在main里&#xff09;&#xff0c;让模块懒加载&#xff08;想加载时才加载&#xff09;&a…

2018.01.01(数字三角形,最长上升子序列等)

2017.12.24 简单的动态规划 1.数字三角形(算法引入) 题目描述&#xff1a;下图所示是一个数字三角形&#xff0c;其中三角形中的数值为正整数&#xff0c;现规定从最顶层往下走到最底层&#xff0c;每一步可沿左斜线向下或右斜线向下走。设三角形有n层&#xff0c;编程计算出从…

Mac iOS 允许从任何来源下载应用并打开

一个快捷的小知识点&#xff0c;mark&#xff01; 允许从任何来源下载应用并打开&#xff0c;不用手动去允许&#xff0c;更加简洁&#xff01; 只需一行命令 sudo spctl --master-disable 1.正常情况下&#xff0c;打开偏好设置&#xff0c;选择安全性与隐私&#xff0c;界面是…

ES5-4 函数基础与种类、形实参及映射、变量类型

模块编程原则&#xff1a;高内聚&#xff0c;低耦合&#xff08;重复部分少&#xff09;&#xff0c;让一个模块有强的功能性、高的独立性 → 单一责任制&#xff0c;用函数进行解耦合。 1. 函数命名规则 不能以数字开头可以以字母_$开头包含数字小驼峰命名法 函数声明一定有…

javascript --- 抽象相等

字符串和数字之间的相等比较 var a 42; var b "42";a b; // false a b; // trueES5规范11.9.3.4-5定义如下: (1)如果Type(x)是数字,Type(y)是字符串,则返回 x ToNumber(y) 的结果 (2)如果Type(x)是字符串,Type(x)是数字,则返回 ToNumber(x) y 的结果// 总结…

Spring加载context的几种方法

Spring中的context管理 Spring中IOC容器的初始化&#xff1a; ApplicationContext即是保存bean对象的容器&#xff0c;故容器本身的初始化&#xff0c;就是通过一系列的配置&#xff0c;将ApplicationContext进行初始化。 而配置ApplicationContext大方向上分为了3中&#xff1…

centos 6.5 配置网络

编辑 vi /etc/sysconfig/network-scripts/ifcfg-eth0修改内容 DEVICE"eth0" BOOTPROTO"static" HWADDR"00:50:56:98:06:D0" IPV6INIT"no" MTU"1500" NM_CONTROLLED"no" ONBOOT"yes" TYPE…