堆排序
堆排序是一种选择排序,遍历后选择数组中最大的值放到堆顶,可用数组实现,最好和最坏和平均复杂度都为O(nlogn),不稳定。
堆就是一种完全二叉树,排序特征是堆顶元素大于子元素(不管子元素之间的排列);完全二叉树的特征是层序遍历等于满二叉树的层序遍历(也就是底层可以没有右边的子树)
用数组来描述堆的定义有大顶堆和小顶堆,若为大顶堆,则i的父节点为i/2,子节点为2i和2i+1
arr[i]<arr[i/2] && arr[i]>arr[2i] && arr[i]>arr[2i+1]
但是放到数组中父节点变成了2(i-1) 子节点为2i+1,2i+2
升序使用大顶堆,就是先将待排序数组构造成一个大顶堆,此时堆顶为最大值,将它放到最后一个(对应数组中就是最后一个值),就找到了最大值,然后将剩下的值再次排序可以找到第二大的值,将它放到堆底,如此反复就可以将所有值排序
堆顶也就是数组0位置,堆尾就是数组的最后一个位置
步骤
1.构造大顶堆的时候就是从第一个非叶节点(也就是倒数第二层,因为从这开始才有子节点i/2-1)开始调整(调整就是将此元素下沉到适合它的位置),然后一直循环到0也就是堆顶,这时所有元素都完成下沉。
2.此时已经构造好大顶堆,所以交换堆顶元素到堆底,下沉堆顶元素,循环继续交换,下沉。此时循环的i就是未调整的长度可变的,每次都下沉堆顶元素0.
调整的操作是实质就是下沉,需要的是数组,下沉元素位置,还有最大的数组长度(也就是除去已经交换完的位置),目的是把arr[i]值下沉到它该去的位置。第一步就是保存arr[i]为temp,进入for循环,从i的左子节点2i+1开始一直到length结束,进入之后首先找到它的两个子节点中大的那一个,如果此时大的那个子节点大于i,就把子节点值赋值给父节点(不是交换,而是把arr[i]值和i值都改变成是arr[j],j),然后接着循环,一直循环到结束或者时子节点值不再大于父节点。for循环执行完之后将temp放回到arr[i]处。
边界条件一定注意
public static void main(String[] argc){
int[] arr={0,6,3,2,14,6,7};
sort(arr);
for (int i=0;i<arr.length;i++){
System.out.println(arr[i]);
}
}
public static void sort(int[] arr) {
for(int i=arr.length/2-1;i>=0;i--){
sink(arr,i,arr.length-1);//传入的是适合数组的
}
for(int i=arr.length-1;i>0;i--){
swap(arr,i,0);
sink(arr,0,i-1);//已经交换了的最后就不用比较,减1的基础上再减1
}
}
public static void sink(int[] arr,int i,int length){
int temp=arr[i];
for(int j=2*i+1;j<length;j++){//最后一个不用下沉了
if(j+1<length && arr[j]<arr[j+1]){
j=j+1;
}
if(arr[j]>temp){
arr[i]=arr[j];
i=j;
}else{
break;
}
}
arr[i]=temp;
}
public static void swap(int[] arr,int lo,int hi){
int temp=arr[lo];
arr[lo]=arr[hi];
arr[hi]=temp;
}
归并排序
归并的操作就是把两个以及排序好的队列归并merge到一起,两个就是二路归并。
时间复杂度为O(nlogn),最好为O(nlogn)最差为O(nlogn),稳定。
所以它的复杂度和堆排序一样,但稳定,所以工程中对象的排序一般用归并排序
过程很简单,就是递归,然后归并操作
归并merge操作很重要,在归并排序中需要的是创建一个新数组,然后遍历旧数组中的两个部分,将值放到新数组中,最后全部遍历完成则将旧数组的值放到新数组中。
merge操作的应用有很多,比如说逆序对问题,比如说小和问题。
static void MergeSortA(int[] a){
if(a.length<=1 || a==null) return;
MergeSort(a,0,a.length-1);
}
public static void MergeSort(int[] arr,int lo,int hi){
if(lo==hi){
return;
}
int mid=lo+((hi-lo)>>1);
MergeSort(arr,lo,mid);
MergeSort(arr,mid+1,hi);
Merge(arr,lo,mid,hi);
}
public static void Merge(int[] arr,int lo,int mid,int hi){
int[] res=new int[hi-lo+1];
int p1=lo;
int p2=mid+1;
int cur=0;
while(p1<=mid && p2<=hi){
if(arr[p1]<=arr[p2]){
res[cur++]=arr[p1++];
}else{
res[cur++]=arr[p2++];
}
}
while(p1<=mid){
res[cur++]=arr[p1++];
}
while(p2<=hi){
res[cur++]=arr[p2++];
}
for(int i=0;i<res.length;i++){
arr[lo+i]=res[i];
}
}
快速排序
快速排序利用的主要是partition操作和递归,每次都先将数组分割成两部分,小于a的放左边,大于a的放右边。然后递归分割这两部分。
与归并排序的区别是归并是先递归,然后归并排好序的两部分。快排是先分割,然后递归分割两部分。
平均复杂度为O(nlogn),最好O(n),最坏O(n2),不稳定,所以工程中排序基本数据类型用的就是快速排序。
比起普通的快速排序,改进的地方主要有借鉴了荷兰国旗问题将partition分成了三部分,第二个改进的地方就是切分的时候是随机一个数来切分的。
partiton操作非常重要,主要思想就是通过遍历把数组分成前后两部分或者是三部分,分成三部分需要两个分界点,分成两部分需要两个分界点。
只需要一个while操作遍历即可完成分割。
public static void main(String[] argc){
int[] arr={0,6,3,2,14,6,7};
sort(arr);
for (int i=0;i<arr.length;i++){
System.out.println(arr[i]);
}
}
public static void sort(int[] arr) {
if(arr==null || arr.length==0){
return;
}
quickSort(arr,0,arr.length-1);
}
public static void quickSort(int[] arr,int lo,int hi){
if(lo>hi){
return;
}
int[] mid=partition(arr,lo,hi);
quickSort(arr,lo,mid[0]-1);
quickSort(arr,mid[1]+1,hi);
}
public static int[] partition(int[] arr,int lo,int hi){
int ran=lo+(int)(Math.random()*(hi-lo+1));//注意这里要加lo
swap(arr,ran,hi);
int less=lo-1;
int more=hi;
int cur=lo;;
while(cur<more){
if(arr[cur]<arr[hi]){
swap(arr,cur++,++less);
}else if(arr[cur]>arr[hi]){
swap(arr,cur,--more);//注意这里的cur是不加的,因为换过来的还没遍历
}else{
cur++;
}
}
swap(arr,hi,more++);
return new int[]{less+1,more-1};
}
public static void swap(int[] arr,int lo,int hi){
int temp=arr[lo];
arr[lo]=arr[hi];
arr[hi]=temp;
}
partition应用也非常多,就是在考虑分组成几部分或者要得到某部分的时候非常有用。
1.比如说第一个是返回超过数组中一半长度的数字,这个可以根据它的特点知道排好序的中位数肯定是这个数字。所以可以利用partition将随机一个数字进行分割,分为前面小于它,后面大于它,中间等于它,如果返回发现中间位置不在等于它的范围内,就缩小范围来循环partition,直到发现这样的数在中间了。再看这个数是否超过一半,超过则就是这个数,不超过则返回0。
2.返回数组中第k大的数字,其实就是一个partition,先随机选一个数字,左边比它小右边比它大,然后如果不在第k个接着partition,直到找到第k个数。
3.比如说找出数组中的前k个数,这个也是要把数组分成两半,先以一个数比如说第k个数partition,分好之后看它的左边有几个数,不管小于还是大于都循环partition直到分好之后下标为k