目录
2.1、大根堆:每个节点的值都大于或者等于他的左右孩子节点的值
2.2、小根堆:每个结点的值都小于或等于其左孩子和右孩子结点的值
一、快速排序
1、概念
快速排序(Quick Sort)是从冒泡排序算法演变而来的,实际上是在冒泡排序基础上的递归分治法。快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。
2、算法原理
这是一个无序数列:4、5、8、1、7、2、6、3,我们要将它按从小到大排序。按照快速排序的思想,我们先选择一个基准元素,进行排序
我们选取4为我们的基准元素,并设置基准元素的位置为index,设置两个指针left和right,分别指向最左和最右两个元素
接着,从right指针开始,把指针所指向的元素和基准元素做比较,如果比基准元素大,则right指针向左移动,如果比基准元素小,则把right所指向的元素填入index中
3和4比较,3比4小,将3填入index中,原来3的位置成为了新的index,同时left右移一位
然后,我们切换left指针进行比较,如果left指向的元素小于基准元素,则left指针向右移动,如果元素大于基准元素,则把left指向的元素填入index中
5和4比较,5比4大,将5填入index中,原来5的位置成为了新的index,同时right左移一位
接下来,我们再切换到right指针进行比较,6和4比较,6比4大,right指针左移一位
2和4比较,2比4小,将2填入index中,原来2的位置成为新的index,left右移一位
随着left右移,right左移,最终left和right重合
此时,我们将基准元素填入index中,这时,基准元素左边的都比基准元素小,右边的都比基准元素大,这一轮交换结束
第一轮,基准元素4将序列分成了两部分,左边小于4,右边大于4,第二轮则是对拆分后的两部分进行比较
此时,我们有两个序列需要比较,分别是3、2、1和7、8、6、5,重新选择左边序列的基准元素为3,右边序列的基准元素为7
第二轮排序结束后,结果如下所示
此时,3、4、7为前两轮的基准元素,是有序的,7的右边只有8一个元素也是有序的,因此,第三轮,我们只需要对1、2和5、6这两个序列进行排序
第三轮排序结果如下所示
至此所有的元素都是有序的
3、代码实现
package com.kgf.algorithm.sort;
/***
* 快速排序
*/
public class QuikSort {
public static void main(String[] args) {
int[] nums = {9,1,2,5,7,4,8,6,3,5};
QuikSort qs = new QuikSort();
qs.quikSort(nums,0,nums.length-1);
for (int num : nums) {
System.out.print(num+"\t");
}
System.out.println();
}
/***
* 快速排序
* @param nums
*/
public void quikSort(int[] nums,int statIndex,int endIndex){
if (statIndex>=endIndex)return;
//进行指针移动,寻找新的pivot
int pivotindex = getPartition(statIndex,endIndex,nums);
quikSort(nums,statIndex,pivotindex-1);
quikSort(nums,pivotindex+1,endIndex);
}
/***
* 进行指针移动
* @param statIndex
* @param endIndex
* @return
*/
private int getPartition(int statIndex, int endIndex,int[] nums) {
//定义一个基准元素
int pivot = nums[statIndex];
//定义左右指针
int left = statIndex,right = endIndex;
int index = statIndex;
while (right>left){
while (right>left &&nums[right]>=pivot){
right--;
}
nums[left] = nums[right];
index = right;
while (right>left && nums[left]<=pivot){
left++;
}
nums[index] = nums[left];
index = left;
}
nums[index] = pivot;
return index;
}
}
二、堆排序
1、简介
堆排序是指利用堆这种数据结构所设计的一种排序算法。
堆(Heap)是一个近似完全二叉树的结构,并同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
一般情况,将堆顶元素为最大值的叫做“大顶堆”(Max Heap),堆顶为最小值的叫做“小顶堆”。
算法简单来说,就是构建一个大顶堆,取堆顶元素作为当前最大值,然后删掉堆顶元素、将最后一个元素换到堆顶位置,进而不断调整大顶堆、继续寻找下一个最大值。
这个过程有一些类似于选择排序(每次都选取当前最大的元素),而由于用到了二叉树结构进行大顶堆的调整,时间复杂度可以降为O(nlogn)。
2、堆分类
2.1、大根堆:每个节点的值都大于或者等于他的左右孩子节点的值
2.2、小根堆:每个结点的值都小于或等于其左孩子和右孩子结点的值
2.3、两种结构映射到数组为:
大根堆:
小根堆:
注意:
父-->子:
i--->左孩子:2*i+1
i--->右孩子:2*i+2
子-->父:
i--->(i-1)/2
3、排序思想
- 首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
- 将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
- 将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
- 注意:升序用大根堆,降序就用小根堆(默认为升序)
4、构造堆
如何构造堆呢?
首先我们给定一个无序的序列,将其看做一个堆结构,一个没有规则的二叉树,将序列里的值按照从上往下,从左到右依次填充到二叉树中。
对于一个完全二叉树,在填满的情况下(非叶子节点都有两个子节点),每一层的元素个数是上一层的二倍,根节点数量是1,所以最后一层的节点数量,一定是之前所有层节点总数+1,所以,我们能找到最后一层的第一个节点的索引,即节点总数/2(根节点索引为0),这也就是第一个叶子节点,所以第一个非叶子节点的索引就是最后一个叶子结点的索引-1。那么对于填不满的二叉树呢?这个计算方式仍然适用,当我们从上往下,从左往右填充二叉树的过程中,第一个叶子节点,一定是序列长度/2, 所以第最后一个非叶子节点的索引就是 arr.len / 2 -1,对于此图数组长度为5,最后一个非叶子节点为5/2-1=1,即为6这个节点。
那么如何构建呢?
我们找到了最后一个非叶子节点,即元素值为6的节点,比较它的左右节点中最大的一个的值,是否比他大,如果大就交换位置。
在这里5小于6,而9大于6,则交换6和9的位置
找到下一个非叶子节点4,用它和它的左右子节点进行比较,4大于3,而4小于9,交换4和9位置
此时发现4小于5和6这两个子节点,我们需要进行调整,左右节点5和6中,6大于5且6大于父节点4,因此交换4和6的位置
此时我们就构造出来一个大根堆,下来进行排序
首先将顶点元素9与末尾元素4交换位置,此时末尾数字为最大值。
排除已经确定的最大元素,将剩下元素重新构建大根堆
一次交换重构如图:
此时元素9已经有序,末尾元素则为4(每调整一次,调整后的尾部元素在下次调整重构时都不能动)
二次交换重构如图:
最终排序结果:
由此,我们可以归纳出堆排序算法的步骤:
- 把无序数组构建成二叉堆。
- 循环删除堆顶元素,移到集合尾部,调节堆产生新的堆顶。
当我们删除一个最大堆的堆顶(并不是完全删除,而是替换到最后面),经过自我调节,第二大的元素就会被交换上来,成为最大堆的新堆顶。
正如上图所示,当我们删除值为9的堆顶节点,经过调节,值为6的新节点就会顶替上来;当我们删除值为6的堆顶节点,经过调节,值为5的新节点就会顶替上来.......
由于二叉堆的这个特性,我们每一次删除旧堆顶,调整后的新堆顶都是大小仅次于旧堆顶的节点。那么我们只要反复删除堆顶,反复调节二叉堆,所得到的集合就成为了一个有序集合,
堆排序是不稳定的排序,空间复杂度为O(1),平均的时间复杂度为O(nlogn),最坏情况下也稳定在O(nlogn)
5、代码实现
package com.kgf.algorithm.sort;
/***
* 堆排序
*/
public class HeapSort {
public static void main(String[] args) {
int[] nums = {4,6,3,5,9};
HeapSort hs = new HeapSort();
hs.heapSort(nums);
for (int num : nums) {
System.out.print(num+"\t");
}
System.out.println();
}
public void heapSort(int[] nums){
//将待排序的数组构建成大顶堆,此时,整个序列的最大值就是堆顶的根节点
int n = nums.length;
//首先根据完全二叉树的规则,找到最后一个非叶子节点索引,然后开始处理
for (int i = n/2-1; i >=0; i--) {
structHeap(nums,i,n);
}
//构建好大顶堆之后,开始进行排序
for (int i = 0; i < n; i++) {
//首先将堆顶移到最后一位
int temp = nums[0];
nums[0] = nums[n-i-1];
nums[n-i-1] = temp;
structHeap(nums,0,n-i-1);
}
}
/***
* 对堆左右节点数据进行比较,交换数据
* @param nums
* @param index
* @param len
*/
private void structHeap(int[] nums,int index,int len) {
//记录当前节点的数据
int temp = nums[index];
//这里的i表示的是子节点的位置,index的左节点是2*index+1,右节点是2*index+2
for (int i = 2*index+1; i < len; i=2*index+1) {
if (nums[i]<nums[i+1] && i+1<len){
i++;
}
if (nums[i]>temp){
//开始进行数据交换
nums[index] = nums[i];
nums[i] = temp;
index = i;
}else {
break;
}
}
}
}