前言
关于排序算法的术语我就不再述说,不清楚的可以看我上一篇博文,在上篇博文我讲了一下关于冒泡、插入、选择排序,这都是非常基础的排序算法,今天我就深入一点,讲讲稍微复杂点的快速、归并以及二叉树-堆排序的原理。
快速排序
快速排序其实很简单,它采用了分治法的方式,何为分治法,就是把一个数组分成两块,取一个基数进行左右比较,这个基数通常是第一个数字,先进行左边比较,比它小,交换位置,再进行右边比较,比它大又交换位置,排完第一次序后,两边数组各自再排,最后完成整个数组快速排序。
不稳定排序
原地排序
时间复杂度O(n)
空间复杂度O(n)
//快速排序 attr需要排序的数组 min基数下标,通常是第一个也就是0 max数组长度
public static int[] kssort(int[] attr,int min,int max){
int key = attr[min];//基数
int start = min;//开始位置
int end = max;//结束位置
while(start<end){//检查数组是否满足循环条件 循环结束跳出
//先从后往前找
while(start<end && key<=attr[end]){
//如果开始位置下标数字大于等于基数,那么结束位置数组下标往前移动一位
end--;
}
//结束位置下标对应数字小于基数 则他们互相交换位置
if(attr[end]<key){
//经过第一次交换位置 基数位置来到了后半段
int temp = attr[end];
attr[end] = attr[start];
attr[start] = temp;
}
//再从前往后找
while(start<end && key>=attr[start]){
//如果开始位置下标数字小于等于基数,那么开始位置数字下标往后移动一位
start++;
}
//如果开始位置下标数字大于基数,则他们互换位置
if(key<attr[start]){
//经过第二次交换位置 基数位置又回到了前半段
int temp = attr[start];
attr[start] = attr[end];
attr[end] = temp;
}
}
//第一次排序完成 进行前半段排序
if(start>min){
//start是基数位置 所以下标要减一位
kssort(attr,min,start-1);
}
//第一次排序完成 进行后半段排序
if(end<max){
//end是基数位置 所以开始位置下标要加一位
kssort(attr,end+1,max);
}
return attr;
}
归并排序
归并排序看名称我想你也能感觉出来点什么,归并排序采用了分治法,称为二路归并,它先是把一个数组分成两个数组,然后对这两个数组自己内部继续分,分到只有一个元素的时候,就开始归并,相邻的两个元素比较,合并,然后再相邻的两个数组比较合并,最后完成最大的两个数组排序,这时候再对这两个大数组进行归并,完成整个数组归并排序,归并的过程很简单,就是取两个已经排完序的数组第一个元素进行比较,最小的元素放到一个新的数组第一位,上一次比较完剩余那个元素和对面数组下一个元素再进行比较,依次下去,剩余最后一位元素时直接放上去。
稳定排序
非原地排序
时间复杂度O(n)
空间复杂度O(n)
//归并排序 attr需要排序的数组 min基数下标,通常是第一个也就是0 max数组长度
public static int[] gbsort(int[] attr,int min,int max){
if(max>min){
int mid = min+(max-min)/2;//取中间值下标
gbsort(attr,min,mid);//左边数组排序
gbsort(attr,mid+1,max);//右边数组排序
int sum = max - min+1;//合并后元素总数
int[] arr = new int[sum];//临时合并的数组
int left = min;//左边有序数组开始下标
int right = mid+1;//右边有序数组开始下标
int idx = 0;//新数组起始下标
//按升序归并到新数组中 坐标小于等于中间值 右边小于等于最大值
while(left<=mid&&right<=max){
arr[idx++] = (attr[left]<=attr[right])?attr[left++]:attr[right++];
}
//右边数组已拷贝完毕,把左边剩余数组直接拷贝到合并数组中
while(left<=mid){
arr[idx++] = attr[left++];
}
//左边数组已拷贝完毕,把右边剩余数组直接拷贝到合并数组中
while(right<=max){
arr[idx++] = attr[right++];
}
//将合并数组拷贝到原来位置
for(int i=0;i<arr.length;i++){
attr[min+i] = arr[i];
}
}
return attr;
}
二叉树-堆排序
堆排序,顾名思义就是先把一个数组构造成一个堆,他是一个近似完全二叉树的结构,这个二叉树堆有什么特点呢,首先分左子树和右子树,顶部只能有一个元素,然后依次往下扩展,每个节点只能拥有两个子元素,直到把数组的元素全部放完,就构造成了一个初始无序的二叉树堆,下一步就需要我们从左子树最下级子节点,拿到最下面的那个子元素,与他的父节点进行比较,以比它大或者比它小的规则,把元素交换位置往上挪,直到最顶部根节点元素为最小或者最大元素,这时候就成功构建了一个大顶堆或者小顶堆,接着把二叉树最下级子节点第一个元素与最顶堆根节点元素交换位置,就得到了第一个最大或者最小元素,接着用二叉树最下级子节点第二个元素重复刚刚与父节点比较的操作,依次下去完成整个堆排序。
不稳定排序
原地排序
时间复杂度O(n)
空间复杂度O(1)
//二叉树-堆排序
public static int[] exsdsort(int[] attr){
//获取数组最后一个元素的下标
int n = attr.length-1;//
int h = n/2;//
//构建一个二叉树大顶堆
for(int i=h;i>=0;i--){
dgdink(attr,i,n);
}
//下沉排序 根节点与最下级节点元素交换位置 然后节点减一重新排序
// 堆的根节点永远是最大值,所以只需将最大值和最后一位的元素交换即可
// 然后再除去最大结点以外的 n-1 的堆,将新堆的根节点放在倒数第二的位置,如此反复
while (n > 0) {
// 将 a[1] 与最大的元素 a[n] 交换
int temp = attr[0];
attr[0] = attr[n];
attr[n] = temp;
// 堆大小减1
n--;
// 下沉排序,剩下元素重新构建大顶堆
dgdink(attr, 0, n);
}
return attr;
}
//递归构造堆
public static void dgdink(int []attr,int k,int n){
//是否存在左孩子节点
while((2*k+1)<=n){
//左孩子的下标
int left = 2*k+1;
// left < n 说明存在右孩子,判断将根节点下沉到左还是右
// 如果左孩子小于右孩子,那么下沉为右子树的根,并且下次从右子树开始判断是否还要下沉
if (left < n && attr[left] < attr[left + 1]){
left = left + 1;
}
// 如果根节点不小于它的子节点,表示这个子树根节点最大
if (attr[k] >= attr[left]){
break; // 直接跳出while循环
}
// 否则就将当前子节点与根节点交换位置,根节点获取最大值
int temp = attr[k];
attr[k] = attr[left];
attr[left] = temp;
// 继续从左子树或右子树开始,判断根节点是否还要下沉,数组内存在更大的值
k = left;
}
}