-
2020-07-01 17:01:46
十大经典排序算法
- 十大经典排序算法-冒泡排序算法详解
- 十大经典排序算法-选择排序算法详解
- 十大经典排序算法-插入排序算法详解
- 十大经典排序算法-希尔排序算法详解
- 十大经典排序算法-快速排序算法详解
- 十大经典排序算法-归并排序算法详解
- 十大经典排序算法-堆排序算法详解
- 十大经典排序算法-计数排序算法详解
- 十大经典排序算法-桶排序算法详解
- 十大经典排序算法-基数排序算法详解
一、什么是堆排序
1.概念
堆排序(Heapsort)是利用二叉堆的概念来排序的选择排序算法,分为两种:
- 升序排序:利用最大堆进行排序
- 降序排序:利用最小堆进行排序
2.算法原理
给定一个最大堆如下图所示,以该最大堆进行演示堆排序
首先,删除堆顶元素10(即最大的元素),并将最后的元素3补充到堆顶,删除的元素10,放置于原来最后的元素3的位置
根据二叉堆的自我调整,第二大的元素9会成为二叉堆新的堆顶
删除元素9,元素8成为最大堆堆顶
删除元素8,元素7成为最大堆堆顶
依次删除最大元素,直至所有元素全部删除
此时,被删除的元素组成了一个从小到大排序的序列
3.算法实现
// 下沉调整 // 最大堆 function downAdjust(arr, parentIndex, length) { // 缓存父节点的值,用于最后进行赋值,而不需要每一步进行交换 let temp = arr[parentIndex]; // 孩子节点下标,暂时定为左孩子节点下标 let childIndex = 2 * parentIndex + 1; while (childIndex < length) { // 当存在右孩子节点,且右孩子节点的值小于左孩子节点的值,childIndex记录为右孩子节点的下标 // childIndex实际记录的是最小的孩子节点的下标 if (childIndex + 1 < length && arr[childIndex + 1] > arr[childIndex]) { childIndex++; } // 如果父节点的值比孩子节点的值都小,则调整结束 if (temp >= arr[childIndex]) { break; } // 将最小的孩子节点的值赋值给父节点 arr[parentIndex] = arr[childIndex]; parentIndex = childIndex; childIndex = 2 * parentIndex + 1; } arr[parentIndex] = temp; } // 堆排序 function sort(arr) { // 把无序数组构建成二叉堆 for (let i = parseInt(arr.length / 2) - 1; i >= 0; i--) { downAdjust(arr, i, arr.length); } console.log(arr); // 循环删除堆顶元素,移到集合尾部,调节堆产生新的堆顶 for (let i = arr.length - 1; i > 0; i--) { // 最后一个元素与第一个元素交换 [arr[0], arr[i]] = [arr[i], arr[0]]; downAdjust(arr, 0, i); } } let arr = [10, 9, 8, 5, 6, 7, 1, 4, 2, 3]; sort(arr); console.log(arr);
三、堆排序算法特点
1.时间复杂度
下沉调整的时间复杂度等同于堆的高度O(logn),构建二叉堆执行下沉调整次数是n/2,循环删除进行下沉调整次数是n-1,时间复杂度约为O(nlogn)
2.空间复杂度
堆排序算法排序过程中需要一个临时变量进行两两交换,所需要的额外空间为1,因此空间复杂度为O(1)
3.稳定性
堆排序算法在排序过程中,相同元素的前后顺序有可能发生改变,所以堆排序是一种不稳定排序算法
另外推荐一个开发者小工具网站,个人觉得里面的Json格式化功能很强大,报错很详细
还可以输入表达式进行内容选取,对于复杂json非常多层级的内容展现非常用用处
更多相关内容 -
解读堆排序算法及用C++实现基于最大堆的堆排序示例
2020-12-25 15:13:391、堆排序定义 n个关键字序列Kl,K2,…,Kn称为堆,当且仅当该序列满足如下性质(简称为堆性质): (1) ki≤K2i且ki≤K2i+1 或(2)Ki≥K2i且ki≥K2i+1(1≤i≤ ) 若将此序列所存储的向量R[1..n]看做是...堆排序算法 2、 -
c语言实现堆排序算法 heapsort
2019-04-15 10:50:09堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。 堆排序的平均时间复杂度为Ο... -
C++堆排序算法的实现方法
2020-12-26 08:28:00首先,由于堆排序算法说起来比较长,所以在这里单独讲一下。堆排序是一种树形选择排序方法,它的特点是:在排序过程中,将L[n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲节点和孩子节点之间的内在... -
最快的排序算法 谁才是最强的排序算法:快速排序-归并排序-堆排序,排序算法数据结构
2022-04-07 15:16:34最快的排序算法 谁才是最强的排序算法:快速排序-归并排序-堆排序,排序算法数据结构 -
C++实现希尔、快速、堆排序、归并排序算法
2021-01-21 15:43:38C++实现希尔、快速、堆排序、归并排序算法,一些中文注释可能成乱码了,但是不影响代码执行。 -
堆排序算法
2017-08-17 20:57:00堆排序 -
挑战七大排序算法-04堆排序
2021-01-07 08:13:59堆排序 1.原理 2.实现 3.性能分析 堆排序 1.原理 基本原理也是选择排序,只是不再使用遍历的方式查找无序区间的最大数,而是通过堆来选择无序区间的最大数 升序:大顶堆;降序:小顶堆 堆排序的基本思路: a.将无需... -
Java 归并排序算法、堆排序算法实例详解
2020-08-30 15:19:03主要介绍了Java 归并排序算法、堆排序算法实例详解,需要的朋友可以参考下 -
堆排序算法(选择排序改进)
2020-09-04 18:15:24主要介绍了堆排序算法(选择排序改进),有需要的朋友可以参考一下 -
六大排序算法:插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序
2021-07-23 21:56:09文章目录:1. 插入排序2.希尔排序 1. 插入排序 步骤: 1.从第一个元素开始,该元素可以认为已经被排序 ... 在待排序的元素中,假设前n-1个元素已有序,现将第n个元素插入到前面已经排好的序列中,使得前n个
1. 插入排序
步骤:
1.从第一个元素开始,该元素可以认为已经被排序
2.取下一个元素tem,从已排序的元素序列从后往前扫描
3.如果该元素大于tem,则将该元素移到下一位
4.重复步骤3,直到找到已排序元素中小于等于tem的元素
5.tem插入到该元素的后面,如果已排序所有元素都大于tem,则将tem插入到下标为0的位置
6.重复步骤2~5动图演示如下:
思路:
在待排序的元素中,假设前n-1个元素已有序,现将第n个元素插入到前面已经排好的序列中,使得前n个元素有序。按照此法对所有元素进行插入,直到整个序列有序。
但我们并不能确定待排元素中究竟哪一部分是有序的,所以我们一开始只能认为第一个元素是有序的,依次将其后面的元素插入到这个有序序列中来,直到整个序列有序为止。
代码如下:void InsertSort(int* arr, int n) { for (int i = 0; i < n - 1; ++i) { //记录有序序列最后一个元素的下标 int end = i; //待插入的元素 int tem = arr[end + 1]; //单趟排 while (end >= 0) { //比插入的数大就向后移 if (tem < arr[end]) { arr[end + 1] = arr[end]; end--; } //比插入的数小,跳出循环 else { break; } } //tem放到比插入的数小的数的后面 arr[end + 1] = tem; //代码执行到此位置有两种情况: //1.待插入元素找到应插入位置(break跳出循环到此) //2.待插入元素比当前有序序列中的所有元素都小(while循环结束后到此) } }
时间复杂度:最坏情况下为O(N*N),此时待排序列为逆序,或者说接近逆序
最好情况下为O(N),此时待排序列为升序,或者说接近升序。
空间复杂度:O(1)2.希尔排序
步骤:
1.先选定一个小于N的整数gap作为第一增量,然后将所有距离为gap的元素分在同一组,并对每一组的元素进行直接插入排序。然后再取一个比第一增量小的整数作为第二增量,重复上述操作…
2.当增量的大小减到1时,就相当于整个序列被分到一组,进行一次直接插入排序,排序完成。
动图如下:
思路:
希尔排序,先将待排序列进行预排序,使待排序列接近有序,然后再对该序列进行一次插入排序,此时插入排序的时间复杂度为O(N),代码如下:
//希尔排序 void ShellSort(int* arr, int n) { int gap = n; while (gap>1) { //每次对gap折半操作 gap = gap / 2; //单趟排序 for (int i = 0; i < n - gap; ++i) { int end = i; int tem = arr[end + gap]; while (end >= 0) { if (tem < arr[end]) { arr[end + gap] = arr[end]; end -= gap; } else { break; } } arr[end + gap] = tem; } } }
时间复杂度平均:O(N^1.3)
空间复杂度:O(1)3.选择排序
思路:
每次从待排序列中选出一个最小值,然后放在序列的起始位置,直到全部待排数据排完即可。
实际上,我们可以一趟选出两个值,一个最大值一个最小值,然后将其放在序列开头和末尾,这样可以使选择排序的效率快一倍。动图如下:
代码如下://选择排序 void swap(int* a, int* b) { int tem = *a; *a = *b; *b = tem; } void SelectSort(int* arr, int n) { //保存参与单趟排序的第一个数和最后一个数的下标 int begin = 0, end = n - 1; while (begin < end) { //保存最大值的下标 int maxi = begin; //保存最小值的下标 int mini = begin; //找出最大值和最小值的下标 for (int i = begin; i <= end; ++i) { if (arr[i] < arr[mini]) { mini = i; } if (arr[i] > arr[maxi]) { maxi = i; } } //最小值放在序列开头 swap(&arr[mini], &arr[begin]); //防止最大的数在begin位置被换走 if (begin == maxi) { maxi = mini; } //最大值放在序列结尾 swap(&arr[maxi], &arr[end]); ++begin; --end; } }
时间复杂度:最坏情况:O(N^2)
最好情况:O(N^2)
空间复杂度:O(1)4.冒泡排序
思路:
左边大于右边交换一趟排下来最大的在右边动图如下:
代码如下://冒泡排序 void BubbleSort(int* arr, int n) { int end = n; while (end) { int flag = 0; for (int i = 1; i < end; ++i) { if (arr[i - 1] > arr[i]) { int tem = arr[i]; arr[i] = arr[i - 1]; arr[i - 1] = tem; flag = 1; } } if (flag == 0) { break; } --end; } }
时间复杂度:最坏情况:O(N^2)
最好情况:O(N)
空间复杂度:O(1)5.堆排序
堆排可看之间这篇博文----->[堆排]
6.快速排序
5.1 hoare版本(左右指针法)
思路:
1、选出一个key,一般是最左边或是最右边的。
2、定义一个begin和一个end,begin从左向右走,end从右向左走。(需要注意的是:若选择最左边的数据作为key,则需要end先走;若选择最右边的数据作为key,则需要bengin先走)。
3、在走的过程中,若end遇到小于key的数,则停下,begin开始走,直到begin遇到一个大于key的数时,将begin和right的内容交换,end再次开始走,如此进行下去,直到begin和end最终相遇,此时将相遇点的内容与key交换即可。(选取最左边的值作为key)
4.此时key的左边都是小于key的数,key的右边都是大于key的数
5.将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作,此时此部分已有序单趟动图如下:
代码如下://快速排序 hoare版本(左右指针法) void QuickSort(int* arr, int begin, int end) { //只有一个数或区间不存在 if (begin >= end) return; int left = begin; int right = end; //选左边为key int keyi = begin; while (begin < end) { //右边选小 等号防止和key值相等 防止顺序begin和end越界 while (arr[end] >= arr[keyi] && begin < end) { --end; } //左边选大 while (arr[begin] <= arr[keyi] && begin < end) { ++begin; } //小的换到右边,大的换到左边 swap(&arr[begin], &arr[end]); } swap(&arr[keyi], &arr[end]); keyi = end; //[left,keyi-1]keyi[keyi+1,right] QuickSort(arr, left, keyi - 1); QuickSort(arr,keyi + 1,right); }
时间复杂度:
快速排序的过程类似于二叉树其高度为logN,每层约有N个数,如下图所示:5.2 挖坑法
5.2.1 递归
思路:
挖坑法思路与hoare版本(左右指针法)思路类似
1.选出一个数据(一般是最左边或是最右边的)存放在key变量中,在该数据位置形成一个坑
2、还是定义一个L和一个R,L从左向右走,R从右向左走。(若在最左边挖坑,则需要R先走;若在最右边挖坑,则需要L先走)后面的思路与hoare版本(左右指针法)思路类似在此处就不说了
单趟动图如下:
代码如下://快速排序法 挖坑法 void QuickSort1(int* arr, int begin, int end) { if (begin >= end) return; int left = begin,right = end; int key = arr[begin]; while (begin < end) { //找小 while (arr[end] >= key && begin < end) { --end; } //小的放到左边的坑里 arr[begin] = arr[end]; //找大 while (arr[begin] <= key && begin < end) { ++begin; } //大的放到右边的坑里 arr[end] = arr[begin]; } arr[begin] = key; int keyi = begin; //[left,keyi-1]keyi[keyi+1,right] QuickSort1(arr, left, keyi - 1); QuickSort1(arr, keyi + 1, right); }
5.2.2 非递归
//单趟排 int PartSort(int* arr, int begin, int end) { int key = arr[begin]; while (begin < end) { while (key <= arr[end] && begin < end) { --end; } arr[begin] = arr[end]; while (key >= arr[begin] && begin < end) { ++begin; } arr[end] = arr[begin]; } arr[begin] = key; int meeti = begin; return meeti; } void QuickSortNoR(int* arr, int begin, int end) { stack<int> st; //先入右边 st.push(end); //再入左边 st.push(begin); while (!st.empty()) { //左区间 int left = st.top(); st.pop(); //右区间 int right = st.top(); st.pop(); //中间数 int mid = PartSort(arr, left, right); //当左区间>=mid-1则证明左区间已经排好序了 if (left < mid - 1) { st.push(mid - 1); st.push(left); } //当mid+1>=右区间则证明右区间已经排好序 if (right > mid + 1) { st.push(right); st.push(mid + 1); } } }
5.3 前后指针法
思路:
1、选出一个key,一般是最左边或是最右边的。
2、起始时,prev指针指向序列开头,cur指针指向prev+1。
3、若cur指向的内容小于key,则prev先向后移动一位,然后交换prev和cur指针指向的内容,然后cur指针++;若cur指向的内容大于key,则cur指针直接++。如此进行下去,直到cur到达end位置,此时将key和++prev指针指向的内容交换即可。经过一次单趟排序,最终也能使得key左边的数据全部都小于key,key右边的数据全部都大于key。
然后也还是将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作
//快速排序法 前后指针版本 void QuickSort2(int* arr, int begin, int end) { if (begin >= end) return; int cur = begin, prev = begin - 1; int keyi = end; while (cur != keyi) { if (arr[cur] < arr[keyi] && ++prev != cur) { swap(&arr[cur], &arr[prev]); } ++cur; } swap(&arr[++prev],&arr[keyi]); keyi = prev; //[begin,keyi -1]keyi[keyi+1,end] QuickSort2(arr, begin, keyi - 1); QuickSort2(arr, keyi + 1, end); }
-
C++堆排序实现算法
2015-06-01 00:44:16简单的堆排序算法:以定长数组为例,动态数组等可以以此类推 -
Swift实现堆排序算法的代码示例
2020-09-02 05:28:58堆排序(HeapSort)是一树形选择排序,堆排序的时间复杂度O(nlogn),这里我们来看一下Swift实现基堆排序算法的代码示例,首先对堆排序算法的基本概念作一个了解: -
堆排序算法实现
2017-03-16 15:59:50根据算法导论第六章实现的堆排序 -
java堆排序原理及算法实现
2020-08-30 20:32:30本篇文章主要介绍了堆排序的简介,定义,算法实现以及堆排序的性质。想要了解的朋友可以参考下 -
堆排序算法(图解详细流程)
2018-08-04 11:21:17堆排序的时间复杂度O(N*logN),额外空间复杂度O(1),是一个不稳定性的排序 目录 一 准备知识 1.1 大根堆和小根堆 二 堆排序基本步骤 2.1 构造堆 2.2固定最大值再构造堆 三 总结 四代码 一 准备知识 堆的...堆排序的时间复杂度O(N*logN),额外空间复杂度O(1),是一个不稳定性的排序
目录
一 准备知识
堆的结构可以分为大根堆和小根堆,是一个完全二叉树,而堆排序是根据堆的这种数据结构设计的一种排序,下面先来看看什么是大根堆和小根堆
1.1 大根堆和小根堆
性质:每个结点的值都大于其左孩子和右孩子结点的值,称之为大根堆;每个结点的值都小于其左孩子和右孩子结点的值,称之为小根堆。如下图
我们对上面的图中每个数都进行了标记,上面的结构映射成数组就变成了下面这个样子
还有一个基本概念:查找数组中某个数的父结点和左右孩子结点,比如已知索引为i的数,那么
1.父结点索引:(i-1)/2(这里计算机中的除以2,省略掉小数)
2.左孩子索引:2*i+1
3.右孩子索引:2*i+2
所以上面两个数组可以脑补成堆结构,因为他们满足堆的定义性质:
大根堆:arr(i)>arr(2*i+1) && arr(i)>arr(2*i+2)
小根堆:arr(i)<arr(2*i+1) && arr(i)<arr(2*i+2)
二 堆排序基本步骤
基本思想:
1.首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
2.将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
3.将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
2.1 构造堆
将无序数组构造成一个大根堆(升序用大根堆,降序就用小根堆)
假设存在以下数组
主要思路:第一次保证0~0位置大根堆结构(废话),第二次保证0~1位置大根堆结构,第三次保证0~2位置大根堆结构...直到保证0~n-1位置大根堆结构(每次新插入的数据都与其父结点进行比较,如果插入的数比父结点大,则与父结点交换,否则一直向上交换,直到小于等于父结点,或者来到了顶端)
插入6的时候,6大于他的父结点3,即arr(1)>arr(0),则交换;此时,保证了0~1位置是大根堆结构,如下图:
(友情提示:待交换的数为蓝色,交换后的数为绿色)
插入8的时候,8大于其父结点6,即arr(2)>arr(0),则交换;此时,保证了0~2位置是大根堆结构,如下图
插入5的时候,5大于其父结点3,则交换,交换之后,5又发现比8小,所以不交换;此时,保证了0~3位置大根堆结构,如下图
插入7的时候,7大于其父结点5,则交换,交换之后,7又发现比8小,所以不交换;此时整个数组已经是大根堆结构
2.2 固定最大值再构造堆
此时,我们已经得到一个大根堆,下面将顶端的数与最后一位数交换,然后将剩余的数再构造成一个大根堆
(友情提示:黑色的为固定好的数字,不再参与排序)
此时最大数8已经来到末尾,则固定不动,后面只需要对顶端的数据进行操作即可,拿顶端的数与其左右孩子较大的数进行比较,如果顶端的数大于其左右孩子较大的数,则停止,如果顶端的数小于其左右孩子较大的数,则交换,然后继续与下面的孩子进行比较
下图中,5的左右孩子中,左孩子7比右孩子6大,则5与7进行比较,发现5<7,则交换;交换后,发现5已经大于他的左孩子,说明剩余的数已经构成大根堆,后面就是重复固定最大值,然后构造大根堆
如下图:顶端数7与末尾数3进行交换,固定好7,
剩余的数开始构造大根堆 ,然后顶端数与末尾数交换,固定最大值再构造大根堆,重复执行上面的操作,最终会得到有序数组
三 总结
到这里,大家应该对堆排序都有了自己的见解,我们对上面的流程总结下:
1、首先将无需数组构造成一个大根堆(新插入的数据与其父结点比较)
2、固定一个最大值,将剩余的数重新构造成一个大根堆,重复这样的过程
四 代码
代码中主要两个方法:
1、将待排序数组构造成一个大根堆(元素上升)
2、固定一个最大值,将剩余的数再构造成一个大根堆(元素下降)
//堆排序 public static void heapSort(int[] arr) { //构造大根堆 heapInsert(arr); int size = arr.length; while (size > 1) { //固定最大值 swap(arr, 0, size - 1); size--; //构造大根堆 heapify(arr, 0, size); } } //构造大根堆(通过新插入的数上升) public static void heapInsert(int[] arr) { for (int i = 0; i < arr.length; i++) { //当前插入的索引 int currentIndex = i; //父结点索引 int fatherIndex = (currentIndex - 1) / 2; //如果当前插入的值大于其父结点的值,则交换值,并且将索引指向父结点 //然后继续和上面的父结点值比较,直到不大于父结点,则退出循环 while (arr[currentIndex] > arr[fatherIndex]) { //交换当前结点与父结点的值 swap(arr, currentIndex, fatherIndex); //将当前索引指向父索引 currentIndex = fatherIndex; //重新计算当前索引的父索引 fatherIndex = (currentIndex - 1) / 2; } } } //将剩余的数构造成大根堆(通过顶端的数下降) public static void heapify(int[] arr, int index, int size) { int left = 2 * index + 1; int right = 2 * index + 2; while (left < size) { int largestIndex; //判断孩子中较大的值的索引(要确保右孩子在size范围之内) if (arr[left] < arr[right] && right < size) { largestIndex = right; } else { largestIndex = left; } //比较父结点的值与孩子中较大的值,并确定最大值的索引 if (arr[index] > arr[largestIndex]) { largestIndex = index; } //如果父结点索引是最大值的索引,那已经是大根堆了,则退出循环 if (index == largestIndex) { break; } //父结点不是最大值,与孩子中较大的值交换 swap(arr, largestIndex, index); //将索引指向孩子中较大的值的索引 index = largestIndex; //重新计算交换之后的孩子的索引 left = 2 * index + 1; right = 2 * index + 2; } } //交换数组中两个元素的值 public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
友情提示:手机观看,可以左右滑动
-
一种改进的堆排序算法
2020-10-17 02:06:22利用堆的性质降低堆排序过程中的数据比较次数,从而在不提高空间复杂度的前提下改进了堆排序算法的效率。通过理论分析得到改进算法在堆重建过程中的数据比较次数是传统堆排序算法的一半,即改进算法的时间复杂度的... -
堆排序算法 C语言实现
2015-07-17 14:38:59C语言实现的堆排序算法。 提供了堆排序算法的一个接口,可以为其它功能提供功能。 -
算法 十大排序 堆排序
2020-12-21 18:39:52十种排序算法——堆排序(小顶堆) 首先要了解什么是堆?小顶堆又是什么?而堆排序是十种排序种唯一种自定义的数据结构 这里的堆就是我们所熟悉的二叉树 而小顶堆又是什么呢? 小顶堆就是根节点比子节点小,子节点比... -
堆排序算法原理
2018-04-22 16:03:01该资源主要介绍了堆排序算法的原理,给定一个整形数组a[]={16,7,3,20,17,8},对其进行堆排序。 首先根据该数组元素构建一个完全二叉树,得 -
Javascript堆排序算法详解
2020-10-25 03:43:31主要介绍了Javascript堆排序算法及其示例,非常实用,需要的朋友可以参考下 -
C#排序算法之堆排序
2020-12-30 16:10:22本文实例为大家分享了C#实现堆排序的具体代码,供大家参考,具体内容如下 代码: /// /// 堆排序方法。 /// /// /// 待排序数组。 /// private void Heapsort(int[] a) { HeapSort_BuildMaxHeap(a); //... -
堆排序代码 《算法导论》
2020-06-05 01:05:25参考《算法导论》这本书写的一个堆排序的代码,我个人是用Visual studio写的。只要一个积分哦 -
详解堆排序算法原理及Java版的代码实现
2020-09-02 05:06:23如果将堆理解为二叉树,那么树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字,堆排序的时间复杂度为O(N*logN),这里我们就来详解堆排序算法原理及Java版的代码实现 -
实验(五)快速排序算法和堆排序算法的设计.doc
2022-05-06 11:24:42实验(五)快速排序算法和堆排序算法的设计.doc -
C++堆排序算法实例详解
2020-08-29 17:06:35主要介绍了C++堆排序算法,简单分析了堆排序算法的原理并结合实例形式分析了C++实现堆排序的具体操作技巧,需要的朋友可以参考下 -
堆排序算法详解
2018-10-14 16:45:41一、堆排序算法原理和动态图解 将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新...一、堆排序算法原理和动态图解
将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次最大值。如此反复执行,就能得到一个有序序列了。这个过程其实就是先构建一个最大/最小二叉堆,然后不停的取出最大/最小元素(头结点),插入到新的队列中,以此达到排序的目的。如下图所示:
二、二叉树定义
要了解堆首先得了解一下二叉树,在计算机科学中,二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。二叉树的每个结点至多只有二棵子树(不存在度大于 2 的结点),二叉树的子树有左右之分,次序不能颠倒。二叉树的第 i 层至多有 2i - 1 个结点;深度为 k 的二叉树至多有 2k - 1 个结点;对任何一棵二叉树 T,如果其终端结点数为 n0,度为 2 的结点数为 n2,则n0 = n2 + 1。二叉树又分为完全二叉树(complete binary tree)和满二叉树(full binary tree)。树和二叉树的三个主要差别:
- 树的结点个数至少为 1,而二叉树的结点个数可以为 0
- 树中结点的最大度数没有限制,而二叉树结点的最大度数为 2
- 树的结点无左、右之分,而二叉树的结点有左、右之分
- 满二叉树:一棵深度为 k,且有 2k - 1 个节点称之为满二叉树,即每一层上的节点数都是最大节点数。如下图b所示:深度为3的满二叉树。
- 完全二叉树:而在一棵二叉树中,除最后一层外,若其余层都是满的,并且最后一层或者是满的,或者是在右边缺少连续若干节点,则此二叉树为完全二叉树(Complete Binary Tree)。如下图a所示:是一个深度为4的完全二叉树。
三、堆的定义
堆(二叉堆)可以视为一棵完全的二叉树,完全二叉树的一个“优秀”的性质是,除了最底层之外,每一层都是满的,这使得堆可以利用数组来表示(普通的一般的二叉树通常用链表作为基本容器表示),每一个结点对应数组中的一个元素。
对于7在数组存放的position=2,而它的子元素6的position=5=2*2[也就是父元素存放的位置]+1、子元素4的position=6=2*2[也就是父元素存放的位置]+2;同样对于11在在数组存放的position=0,而它的子元素10的position=1=2*0[也就是父元素存放的位置]+1、子元素7的position=2=2*0[也就是父元素存放的位置]+2;所以对于i个元素,它的左右子节点在下标以0开始的数组中的位置分别为:2*i+1、2*i+2。那脑补一下,对于不完全二叉树,如果用数组来存放会有什么问题呢?当然是中间有很多空的元素啦,所以说对于不完全二叉树最好是用链表来存储~。
堆的构建过程示例:建堆的核心内容是调整堆,使二叉树满足堆的定义(每个节点的值都不大于其父节点的值)。调堆的过程应该从最后一个非叶子节点开始,假设有数组A = {1, 3, 4, 5, 7, 2, 6, 8, 0}。那么调堆的过程如下图,数组下标从0开始,A[3] = 5开始。分别与左孩子和右孩子比较大小,如果A[3]最大,则不用调整,否则和孩子中的值最大的一个交换位置,在图1中是A[7] > A[3] > A[8],所以A[3]与A[7]对换,从图1.1转到图1.2。
二叉堆(英语:binary heap)是一种特殊的堆,二叉堆是完全二叉树或者是近似完全二叉树。二叉堆满足堆特性:父节点的键值总是保持固定的序关系于任何一个子节点的键值,且每个节点的左子树和右子树都是一个二叉堆。当父节点的键值总是大于或等于任何一个子节点的键值时为最大堆。 当父节点的键值总是小于或等于任何一个子节点的键值时为最小堆。二叉堆一般用数组来表示。如果根节点在数组中的位置是1,第n个位置的子节点分别在2n和 2n+1。因此,第1个位置的子节点在2和3,第2个位置的子节点在4和5。以此类推。这种基于1的数组存储方式便于寻找父节点和子节点。如果存储数组的下标基于0,那么下标为i的节点的子节点是2i + 1与2i + 2;其父节点的下标是⌊floor((i − 1) ∕ 2)⌋。函数floor(x)的功能是“向下取整”,或者说“向下舍入”,即取不大于x的最大整数(与“四舍五入”不同,向下取整是直接取按照数轴上最接近要求值的左边值,即不大于要求值的最大的那个值)。比如floor(1.1)、floor(1.9)都返回1。对于堆定义中的堆结构插入元素:对于二叉堆来说,要插入一个新元素其整个过程是怎么样的呢?这里还是以我们之前的那个二叉堆进行说明,以插入"9"为例:
目前肯定不满足二叉堆的要求,父接点6是小于新插入的节点9的,所以两者进行位置交换:
同样的思路,父节点7比子节点9要小,所以需要调换位置:
至此元素插入完成,也符合二叉堆父元素大于子元素的规则,从添加过程中可以发现:只需更改待比较的元素,其它的任何元素位置不需要动,所以效率还是很高的。对于堆定义中的堆结构删除元素:这里以删除根结点为例【因为删除根节点是最重要的,所以以它为例】,整个过程如下:
这时当然是不符合二叉堆的规则,接着这样来做:
同理继续进行处理:
继续:
经过这些动作之后就将一个根结点给删除掉了,可以发现其实跟插入一个元素一样,只需更改待比较的元素,其它的任何元素位置不需要动,那像这种每次移除掉最大的值有啥用呢?堆排序就产生了,因为每次从根节点拿肯定是最大的数【以最大堆来说】,这样拿出来的数就成了一个有序的数列了。注意:对于一个很大的堆,这种存储是低效的。因为节点的子节点很可能在另外一个内存页中。B-heap是一种效率更高的存储方式,把每个子树放到同一内存页。如果用指针链表存储堆,那么需要能访问叶节点的方法。可以对二叉树“穿线”(threading)方式,来依序遍历这些节点。
四、堆排序动态示例图
五、堆排序Java代码实现
package com.luna.sort; public class HeapSortMaxAndMin{ public static void main(String[] args) { int[] array = { 19, 38, 7, 36, 5, 5, 3, 2, 1, 0, 56 }; System.out.println("排序前:"); for (int i = 0; i < array.length; i++) { System.out.print(array[i] + ","); } System.out.println(); System.out.println("分割线---------------"); heapSort(array); System.out.println("排序后:"); for (int i = 0; i < array.length; i++) { System.out.print(array[i] + ","); } } public static void heapSort(int[] array) { if (array == null || array.length == 1) return; buildArrayToHeap(array); //将数组元素转化为大顶堆/小顶堆 for (int i = array.length - 1; i >= 1; i--) { // 经过上面的一些列操作,目前array[0]是当前数组里最大的元素,需要和末尾的元素交换,然后拿出最大的元素 swap(array, 0, i); /** * 交换完后,下次遍历的时候,就应该跳过最后一个元素,也就是最大的那个 * 值,然后开始重新构建最大堆堆的大小就减去1,然后从0的位置开始最大堆 */ // buildMaxHeap(array, i, 0); buildMinHeap(array, i, 0); } } // 构建堆 public static void buildArrayToHeap(int[] array) { if (array == null || array.length == 1) return; //递推公式就是 int root = 2*i, int left = 2*i+1, int right = 2*i+2; int cursor = array.length / 2; for (int i = cursor; i >= 0; i--) { // 这样for循环下,就可以第一次排序完成 // buildMaxHeap(array, array.length, i); buildMinHeap(array, array.length, i); } } //大顶堆 public static void buildMaxHeap(int[] array, int heapSieze, int index) { int left = index * 2 + 1; // 左子节点 int right = index * 2 + 2; // 右子节点 int maxValue = index; // 暂时定在Index的位置就是最大值 // 如果左子节点的值,比当前最大的值大,就把最大值的位置换成左子节点的位置 if (left < heapSieze && array[left] > array[maxValue]) { maxValue = left; } // 如果右子节点的值,比当前最大的值大,就把最大值的位置换成右子节点的位置 if (right < heapSieze && array[right] > array[maxValue]) { maxValue = right; } // 如果不相等说明,这个子节点的值有比自己大的,位置发生了交换了位置 if (maxValue != index) { swap(array, index, maxValue); // 就要交换位置元素 // 交换完位置后还需要判断子节点是否打破了最大堆的性质。最大堆性质:两个子节点都比父节点小。 buildMaxHeap(array, heapSieze, maxValue); } } //小顶堆 public static void buildMinHeap(int[] array, int heapSieze, int index) { int left = index * 2 + 1; // 左子节点 int right = index * 2 + 2; // 右子节点 int maxValue = index; // 暂时定在Index的位置就是最小值 // 如果左子节点的值,比当前最小的值小,就把最小值的位置换成左子节点的位置 if (left < heapSieze && array[left] < array[maxValue]) { maxValue = left; } // 如果右子节点的值,比当前最小的值小,就把最小值的位置换成左子节点的位置 if (right < heapSieze && array[right] < array[maxValue]) { maxValue = right; } // 如果不相等说明这个子节点的值有比自己小的,位置发生了交换了位置 if (maxValue != index) { swap(array, index, maxValue); // 就要交换位置元素 // 交换完位置后还需要判断子节点是否打破了最小堆的性质。最小性质:两个子节点都比父节点大。 buildMinHeap(array, heapSieze, maxValue); } } // 数组元素交换 public static void swap(int[] a, int i, int j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; } }
大顶堆优化实现算法:
import java.util.Arrays; public class MaxHeapSort { private int[] arr; public MaxHeapSort(int[] arr){ this.arr = arr; } /** * 堆排序的主要入口方法,共两步。 */ public void sort(){ /* * 第一步:将数组堆化 * beginIndex = 第一个非叶子节点。 * 从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。 * 叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。 */ int len = arr.length - 1; int beginIndex = (len - 1) >> 1; for(int i = beginIndex; i >= 0; i--){ maxHeapify(i, len); } /* * 第二步:对堆化数据排序 * 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,同时遍历长度 - 1。 * 然后从新整理被换到根节点的末尾元素,使其符合堆的特性。 * 直至未排序的堆长度为 0。 */ for(int i = len; i > 0; i--){ swap(0, i); maxHeapify(0, i - 1); } } private void swap(int i,int j){ int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } /** * 调整索引为 index 处的数据,使其符合堆的特性。 * @param index 需要堆化处理的数据的索引 * @param len 未排序的堆(数组)的长度 */ private void maxHeapify(int index,int len){ int li = (index << 1) + 1; // 左子节点索引 int ri = li + 1; // 右子节点索引 int cMax = li; // 子节点值最大索引,默认左子节点。 if(li > len) return; // 左子节点索引超出计算范围,直接返回。 if(ri <= len && arr[ri] > arr[li]) // 先判断左右子节点,哪个较大。 cMax = ri; if(arr[cMax] > arr[index]){ swap(cMax, index); // 如果父节点被子节点调换, maxHeapify(cMax, len); // 则需要继续判断换下后的父节点是否符合堆的特性。 } } /** * 测试用例 * 输出: * [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9] */ public static void main(String[] args) { int[] arr = new int[]{3,5,3,0,8,6,1,5,8,6,2,4,9,4,7,0,1,8,9,7,3,1,2,5,9,7,4,0,2,6}; new MaxHeapSort(arr).sort(); System.out.println(Arrays.toString(arr)); } }
原文地址:https://blog.csdn.net/u011635492/article/details/83046477