冒泡、插入、排序这些排序时间复杂度都是 O(n2),时间复杂度高,适合小数据规模处理。
对于大数据规模排序,用归并排序和快速排序比较适合。
归并排序和快速排序都使用了分而治之的思想。
归并排序
归并排序的思想就是将数组分成前后两部分,然后再对分开的两部分再分成两部分,以此类推,分到不可再分为止,再合并一起,如此数组便可成有序了。
这个先分两半、再把两半分成两半、再分成两半,这就是前面所讲述的递归思想。所以可以用递归完成归并排序。
归并排序思想是分治,分而治之,将大问题分为小问题,小问题解决了,大问题也解决了。
分治思想是一种解决问题的思想,递归是一种编程技巧,两者并不冲突。
下面用递归处理归并排序:
1.递归公式
merge_sort(p…r) = merge(merge_sort(p…q),merge_sort(q+1…r))这里的q是中间元素下标,代表对半
2.终止条件:
p>=r的时候终止
p、r是无序数组的初始坐标和末元素坐标,将一整个排序问题化为了两个子问题,一个是merge_sort(p…q)和merge_sort(q+1…r)。用递归处理子问题,当子问题处理好了,再进行合并,这样p到r的数据大问题也处理好了。
代码如下:
public static void merge(int[] arr, int start, int mid,int end){
int i = start; //左边遍历起点
int j = mid+1; //右边遍历起点
int[] tmp = new int[end-start+1]; //临时数组
int tmp_i = 0; //临时数组起点坐标
while (i<=mid&&j<=end){ //左边从起点到mid,右边mid+1到end遍历,两边一起开始遍历
if (arr[i]<arr[j]){ //把小的放入数组如果右边比左边大,就先把小的左边放入临时数组
tmp[tmp_i++] = arr[i++];//把小的,左边放入数组
}else {
tmp[tmp_i++] = arr[j++]; //把小的右边放入数组
}
}
while (i<=mid){ 上面遍历完后,左边还有值没放入数组,直接放
tmp[tmp_i++] = arr[i++];
}
while (j<=end){上面遍历完后,右边还有值没放入数组,直接放
tmp[tmp_i++] = arr[j++];
}
for (i=0; i<tmp_i;++i){ //遍历临时数组,替换原数组
arr[start++] = tmp[i];
}
}
public static void merger_up_todown(int[] arr, int start, int end){
if (start<end){//递归结束条件
int mid= (start+end)/2;
merger_up_todown(arr,start,mid); //递归左边
merger_up_todown(arr,mid+1,end);//递归右边
merge(arr,start,mid,end);//合并
}
}
先从merger_up_todown这个方法看,里面递归了三个merge,
前面两个merger_up_todown将数组的两半分别进行了拆分然后合并成有序的子数组
最后的一个merge将前面的两个有序子数组进行最后的合并成一个有序的数组.
整体过程如下:
1.申请一个tmp临时数组,大小和无序数组相同arr。
2.然后申请两个坐标i和j代表了了初始元素i和中间元素+1。代表了将数组分为两半的起始点元素,即A[p…q]和 A[q+1…r]
3.比较两个元素A[i]和 A[j],哪个小就放入临时数组tmp,且i或者j后移一位。
4.上面第三步操作进行循环,直到一个子数组的所有数组都放进了临时数组,再把另一个数组的剩余元素全部依次放入临时数组末尾。这个时候临时数组存放的就是两个子数组合并的最后结果了,再把临时数组的数据拷贝到无序数组arr中。
归并排序性能分析
归并排序是稳定的排序算法吗
合并过程中,如果A[p…q]和 A[q+1…r]之间有值相同的元素,先将A[p…q]的元素放入临时数组中,这样就没有改变相同元素的位置,所以是稳定的。
归并排序时间复杂度
假设归并排序中,问题a分解为求解问题b和问题c,等b和c解决后再合并b和c
求解a问题的时间是T(a),求解b和c的时间是 T(b) 和 T( c)
那么
T(1) =C ;C代表常量时间
(a) = T(b) + T© + K
K是合并两个问题bc的时间
假设对n个元素归并排序时间的T(n),
那么两个分解子问题的时间是T(n/2),merge合并两个有序子数组的时间复杂度是O(n)
所以归并时间复杂度的计算公式是
T(n) = 2T(n/2) + n; n>1
即
T(n) = 2T(n/2) + n
= 2*(2T(n/4) + n/2) + n = 4T(n/4) + 2n
= 4(2T(n/8) + n/4) + 2n = 8T(n/8) + 3n
= 8*(2T(n/16) + n/8) + 3n = 16T(n/16) + 4n
…
= 2^k * T(n/2^k) + k * n
…
T(n) =2^k * T(n/2^k) + k * n。
当T(n/2^k)=T(1) 时
即n/2^k=1 ,所以k=log2n ,将K带入公式,得到T(n)=Cn+nlog2n,换成O(n),T(n) 就等于 O(nlogn)
所以归并排序的时间复杂度是O(nlogn)。这个时间复杂度是稳定的,包括了最好、最坏、平均时间复杂度都是O(nlogn)。
空间复杂度
这是归并排序的弱点,归并排序不是原地排序。
归并排序在排序的时候需要借助一个临时数组tmp,这个数组大小跟原无序数组一样是n,所以空间复杂度是O(n)。
快排
快速排序的核心核心思想是分治,例如要排序下坐标p到r数组的数据,然后规定p-r之间一个点叫分区点pivot,使得分区点左边的数是比分区点上的数小的,即arr[p]<arr[pivot] ,分区点右边的数比分区点上的数大,即arr[pivot] < arr[r],然后通过递归、分治思想取缩减p到pivot的距离、pivot到r的距离,使得区间变为1 ,此时数据有序。
如何编排数据使得左边的数比分区点小,右边的数比分区点大,通常油三种方法,如下:
package com.company;
import java.util.Arrays;
public class MyquickSort {
public static void main(String[] args) {
int[] arr1 = {0,11111,103,100,1,3,2,6,4,5,8,7,10,9};
quecksort3(arr1,0,arr1.length-1);
System.out.println(Arrays.toString(arr1));
}
public static void quicksort1(int[] arr, int begin, int end){
//左右指针法
/**
* 1.选出一个key,一般是最左或者最右
* 2.定义begin,end,begin从左往右走,end从右往左走
* 3.key在左边的话,end先走。key在右边的话end先走
* 4.走的过程中,如果end遇到的数小于key遇到的数,停下此时为arr[end]
* 5.end停下后,begin也往前走,如果遇到大于key的数, 也停下,此时为arr[begin]
* 6.arr[end]和arr[begin]进行交换
* 7.交换后end继续往左,如果遇到比key的数小,停下
* 8.begin往右走,如果遇到比key大的数,停下,交换。
* 9.如果在第8的时候没有遇到比key大的数,begin就会遇到end。就是begin=end
* 10.这个时候,将key跟begin对应的数进行交换,交换后,新key左边是比key小的数,右边的数是比key大的数
* **/
if (begin >= end){
return;
}
/*
* begin,0, end arr.length-1
* */
int left = begin; //定义一个左变量,留着做递归
int right = end; //定义一个右变量,留着做递归
int keyi = begin; //定义key所在的下坐标。
while (begin < end){ //当begin<end的时候遍历,当begin=end的时候跳出循环,跟arr[begin]跟arr[keyi]交换
while (arr[end] >= arr[keyi] && begin <end){ --end;} //找出当begin<end的时候,arr[end]<arr[keyi]的下坐标,即在右边找出比key小的值
while (arr[begin] <= arr[keyi] && begin < end){ ++begin;}//跟上同理,在左边找出比key大的值
int tmp = arr[end]; // 此三行是将上面找出的右边大值arr[end]、左边小值arr[begin]进行交换
arr[end] = arr[begin];
arr[begin]=tmp;
}
//经过上面的while循环后,这时候begin跟end指针已经相遇,即begin = end,所以这时候需要将begin坐标对应的值跟key值进行交换
int tmp = arr[end];
arr[end] = arr[keyi];
arr[keyi] = tmp;
keyi = end; //将begin和end相遇的下坐标赋值给keyi,作为数据切割点,切割左边的数据做下一次的递归,切割左边的数据做递归
quicksort1(arr,left,keyi-1);
quicksort1(arr,keyi+1,right);
}
public static void quicksort2(int[] arr, int begin, int end){
//挖坑法
/**
* 1.定义一个单独变量key保存key值(是key值,不是下坐标),一般是数组最左或者最右的的一个元素
* 2.1完成后,key值对应的坐标可以看成是一个坑位,可以随意用别的数填取(即赋值给该下坐标)
* 3.定义两个指针begin\end对应数组的0坐标和最末尾坐标
* 4.如果是选最左(即arr[begin],数组的下坐标0)作为key值,即坑位是arr[begin],因为arr[begin]的值已经赋值给了key,不受数组影响了,先从end--遍历
* 5.如果end--遍历,找到比key值小的arr[end],这时候需要arr[end]赋值给4说的坑位,arr[begin] = arr[end],俗称填坑。赋值填坑完成后,此时的arr[end]就变成新的坑位
* 6.end填坑后,开始begin++遍历,找出比key大的值arr[begin],然后将该值赋值给5的arr[end]坑位进行填坑,此时的arr[begin]就变成了新坑位。
* 7.begin填坑后,再次循环做5、6步骤,直至begin和end在一个坑位相遇(end/begin填坑后,end/begin位置留下坑位,然后另一个指针begin/end遍历++/--找不到比key大或者小的值,一直遍历到begin=end)
* 8.相遇后,下坐标就是坑位,将key值赋值到坑位中,
* 9.将该坑位定义给keyi,进行分割数据分别做递归
* */
if (begin>=end) {return;}
int left = begin;//定义一个左变量,留着做递归
int right = end;//定义一个右变量,留着做递归
int key = arr[begin]; //将数组最左位arr[0]定义并赋值给变量key,留下坑位
while (begin<end){//左右指针遍历
while (arr[end] >=key && begin < end){ --end; } //end找出指针比key小的值,
arr[begin] = arr[end]; //将end找出的比key小的值赋值给坑位arr[begin]进行填坑,留下坑位arr[begin]
while (arr[begin] <= key && begin < end){++begin;}//end填坑后,begin++找出比key大的值,
arr[end] = arr[begin];//将begin找出的值赋值给arr[begin]进行填坑,留下坑位
}
//begin跟end相遇在某个坑位
arr[begin] = key; //用key填坑
int keyi = begin; //将此次处理的最后坑位做数据切割点,切分数据
quicksort2(arr,left,keyi-1);//切分数据,左边递归
quicksort2(arr,keyi+1,right);//切分数据,右边递归
}
public static void quecksort3(int[] arr, int begin, int end){
//前后指针
/**
* 1.选出一个key,用keyi记录它的下坐标,一般最右或者最左。
* 2.定义pre之前指向数组开头(在一开始的时候,pre指向begin-1),cur指针指向pre+1(begin)
* 3.首先arr[cur]会跟key值arr[keyi]比较,如果arr[cur]比key值小,pre指针则会往后移动一位,即pre+1,然后arr[pre+1]跟arr[cur]交换数据,交换完后cur继续往后cur++
* 4.如果arr[cur]比key值大,则cur一直往后走,一直cur++
* 5.cur++一直往后走, 直到cur到end位置,即cur=end
* 6.cur=end后将key值arr[keyi]和++pre值arr[++pre]交换,
* 7.然后现在pre左边是比它小的数,右边是比他大的数,所以将此时的pre赋值给keyi,切分数据,递归
* */
if (begin >= end){return;}
int pre = begin - 1,cur = begin;
int keyi = end;
while (cur != end){
System.out.println("pre:"+pre);
System.out.println("cur:"+cur);
System.out.println("stop");
if (arr[cur] < arr[keyi] && ++pre !=cur){
int tmp = arr[pre];
arr[pre] = arr[cur];
arr[cur] = tmp;
}
++cur;
}
int tmp1 = arr[keyi];
arr[keyi] = arr[++pre];
arr[pre] = tmp1;
keyi = pre;
quecksort3(arr,begin,keyi-1);
quecksort3(arr,keyi+1,end);
}
}
归并排序对比快排
归并排序和快排都是使用了分治、递归的思想去进行实现。理解归并排序重要的是理解递归公式和merge()合并函数,理解快排重要的是理解递归公式和partition()方式。
归并排序是一种时间复杂度比较稳定的排序方法,缺点是不是原地排序,空间复杂度高,O(N)
快排最坏时间复杂度是O(n2),但是平均复杂度是O(nlogn)。而且很小概率演变为最坏时间复杂度。
快排处理第k大元素
package main.java.java_test;
import java.util.Arrays;
/**
* @discreption:
* @author: Chen
* @date: 2022年01月15日 22:13
* @version: 2022年01月15日 admin
*/
public class 第k大元素 {
public static void main(String[] args) {
int[] arr1 = {0,11111,103,100,1,3,2,6,4,5,8,7,10,9};
int result = quickfindLarge(arr1,3);
System.out.println(Arrays.toString(arr1));
System.out.println(result);
}
public static int quickfindLarge(int[] arr, int k){
k = arr.length-k; //在有序序列中找序号为arr.length-k大的数
int begin = 0,end = arr.length-1; //定义遍历数组
while (begin < end){ //左右指针便利
int keyi = quickPartition(arr,begin,end); //找出每次的分区点
if (keyi == k){ //当分区点==k的时候,为第几大元素,如k=1,为最大的数
break;
}else if (keyi < k){ //当分区点在k左边,这个时候arr[keyi]的数不够大,需要左指针右移再分区一次,
begin = keyi+1;
}else { //分区点在k右边,这个时候arr[keyi]太大,需要右指针左移一次
end = keyi-1;
}
}
return arr[k];
}
//快排分区方法,切分数据,使得分区点左边数据小于分区点上的数据,分区点右边的数据大于分区点数据
public static int quickPartition(int[] arr, int begin, int end){
int keyi = begin; // 设置数组最左边为分区点
while (begin < end){ // 左右指针便利
while (arr[end] >= arr[keyi] && begin < end){ --end;} //找出右指针比分区点小的数
while (arr[begin] <= arr[keyi] && begin < end){ ++begin;} //找出左指针比分区点大的数
int tmp = arr[end]; // 交换数据
arr[end] = arr[begin];
arr[begin] = tmp;
}
int temp = arr[end]; //左右指针相遇,移动分区点到相遇点
arr[end] = arr[keyi];
arr[keyi] = temp;
keyi = end; //找出最新的分区点
return keyi;
}
}
在处理第k大元素的时候,需要将之前快排的代码稍微改一下,将原本合并递归的代码修改为两个方法,一个是分区方法,即将数据左右分区,左分区的数小于分区点,右分区的数大于分区点, 另一个方法是寻找第k大分区点的方法,将分区方法求出来的分区点所在的位置与k相比,当分区点=k的时候,此时的arr[分区点]为第k大的数