快速排序:
1)时间复杂度最好情况下:每一次可以均匀的分割待排序序列,也就是说基准值就是中位数,那么时间复杂度就是O(N*logN)
2)时间复杂度最坏情况下:数据有序或者是完全逆序的情况下,时间复杂度可以达到O(N^2),递归的层数太多,树的高度太高,那么会导致递归开辟的栈空间太多,所以导致栈空间被挤爆
3)空间复杂度最好情况下:就是树的高度,就是N*logN
4)空间复杂度最坏的情况下:就是O(N),就是单分支的一棵树
第一种优化的方法:选择基准
1)选取数组中的第一个元素作为基准
2)随机选取基准法,会在有序的情况下会尽可能地把数据分割开,左边会有一定数据的,有可能每一次随机的数据,在极端情况下也会出现单分支的树,满足人品的需求的
3)三数取中法:我们进行选取array[left],array[right]和array[mid],我们进行选取他们的中间的元素作为我们的基准值
4)我们可以把和基准相同的数据移动到跟前
第二种优化的方法:当我们的区间的范围在不断缩小的过程中,我们是可以进行在这段区间进行直接插入排序
我们说当数据有序或者逆序的情况下,我们进行快排所分割出来的是一棵单分支的树,时间复杂度可以达到O(N^2),空间复杂度可以达到O(N)---树的高度
1)当每次划分元素的时候,左右两边的元素的个数相等,这个时候,是快速排序最好的一种情况,其时间复杂度为O(nlogn),与归并排序类似
2)当每次划分元素的时候,一边的元素个数为0,这个时候,是快速排序最坏的一种情况,其时间复杂度为O(n2),此时,序列通常为有序。
一:快速排序的优化
随机选取一个数和当前数组的第一个元素进行交换,这样的交换会使有序的情况下尽可能地分割开
1)我们说先说一下随机选取基准的情况下,例如我要进行1,2,3,4的排序,如果我们选取1为基准,每次进行快排就会得到一个单分支的树,这时的时间复杂度就会达到O(N^2);此时如果我们进行交换1,3的顺序,选取3为基准,那么此时快排就会快很多;
2)但是说随机选取数据是有风险的,例如说原来的数据是3,2,1,4,那么此时我恰好把3,1的数据进行了交换,此时的时间复杂度就有变成O(N^2);
1)三数取中原则(选取中间值,数组的第一个元素,数组的最后一个元素,三个值当中选取中间值作为基准值),在第一次找基准之前,就要使用三树取中原则--------保证肯定有一个数字或者是两个数字在我基准值的左边或者
2)每次递归的过程中,数据的区间在不断地缩小,区间内的数据,也在不断趋近于有序,这时我们就可以对区间上的数据,进行直接插入排序;越有序越快我们可以自己规定一下,如果一段排序区间的数据(end-start+1<100),我们就可以直接进行直接插入排序;
但是我们要注意的是,每对一段区间进行快排之后,一定要进行return操作,应为这段区间的数据就不用接下来继续递归了;
public static int quicksort(int[] arr1,int begin,int end) {
int low = begin;
int high = end;
int temp = arr1[low];
while (low < high) {
while ((low < high)&&(arr1[high]>=temp)) {
high--;//这里的第二个条件是大于等于
}
arr1[low]=arr1[high];
while((low<high)&&(arr1[low]<=temp))
{
low++;
}
arr1[high]=arr1[low];
}
arr1[low]=temp;
return low;
}
public static void strengthsort(int begin,int end,int[] arr1)
{
for(int i=begin+1;i<=end;i++)
{ int tmp=arr1[i];
int j=i-1;
for(j=i-1;j>=begin;j--)
{
if(arr1[j]<tmp)
{
arr1[j+1]=arr1[j];
}
else{
break;
}
}
arr1[j+1]=tmp;
}
}
public static void swap1(int []arr1,int i,int j)
{
int tew=arr1[i];
arr1[i]=arr1[j];
arr1[j]=tew;
}
public static void swap(int []arr1,int begin,int end,int middle)
{
if(arr1[middle]>arr1[begin])
{
swap1(arr1,middle,begin);
}
if(arr1[begin]>arr1[end])
{
swap1(arr1,end,begin);
}
if (arr1[middle] > arr1[end]){
swap1(arr1,middle,end);
}
}
public static void sort(int []arr1,int begin,int end)
{
//1.这是递归的终止条件
if(begin>=end) return;
//2.进行三数取中,保证基准值是中间,开头和结尾的中间的数
int middle=(begin+end)/2;
swap(arr1,begin,end,middle);
//3.因为我们在使用快排来进行排序的时候,数据趋于有序那么就直接使用直接插入排序,在我们进行优化的时候是一定要注意区间的范围
if(end-begin+1<100)
{
strengthsort(begin,end,arr1);
return;
}
//4.进行正常的快排的递归
int mid=quicksort(arr1,begin,end);//注意,这里的begin和end一开始并不可以传0和arr1.length-1,一定要传begin和end
sort(arr1,begin,mid-1);
sort(arr1,mid+1,end);
}
}
ublic class Main{ public static int Sort(int[] array,int low,int high){ int key=low; int temp=array[low]; while(low<high){ while(low<high&&array[high]>=temp){ high--; } while(low<high&&array[low]<=temp){ low++; } int index=array[low]; array[low]=array[high]; array[high]=index; } int tmp=array[low]; array[low]=array[key]; array[key]=tmp; return low; } public static void QuickSort(int[] array,int begin,int end){ if(begin>=end){ return; } if(end-begin+1<100){ DriectSort(array,begin,end); return; } int mid=(begin+end)/2; int index=GetMidData(array,begin,end,mid); int temp=array[index]; array[index]=array[begin]; array[begin]=temp; int privot=Sort(array,begin,end); QuickSort(array,begin,privot-1); QuickSort(array,privot+1,end); } private static int GetMidData(int[] array, int begin, int end, int mid) { if(array[begin]<=array[end]){ if(array[mid]>array[end]){ return end; }else if(array[mid]<array[begin]){ return begin; }else{ return mid; } } if(array[begin]>=array[end]){ if(array[mid]>array[begin]){ return begin; }else if(array[end]>array[mid]){ return end; }else{ return mid; } } return -1; } private static void DriectSort(int[] array, int begin, int end) { for(int i=begin+1;i<=end;i++){ int j=i-1; int temp=array[i]; for(j=i-1;j>=begin;j--){ if(array[j]>temp){ array[j+1]=array[j]; }else{ break; } } array[j+1]=temp; } } }
2)非递归实现快速排序
非递归实现快速排序,要应用的数据结构就是栈;下面是我的思路:
1)调用一次快速排序的方法(quicksort),找到中间基准(mid),把0~mid-1和mid+1~end放到栈里面(第一次放的是两个范围)
2)判断栈是否为空,如果不为空,就弹出栈顶两个元素,注意:放的顺序决定了取的顺序,第一个元素是给high还是给low;
1)什么时候要入栈?当这个区间起码至少有两个元素的时候(mid>low+1)(mid<high-1),因为此时边界的两个数已经有序了,就没有必要进行快速排序了;
2)也就是说经过一次快速排序之后基准值的左边只有一个数据,或者右边只有一个数据就没有必要进行栈操作了,因为左边只有一个数据或者右边只有一个数据那么就说明在基准值左面的这个元素一定比基准值小,在基准值右边的那一个元素一定比基准值大,那么此时我们就没有必要在这段区间内在次放入到栈里面将来弹出栈元素进行快速排序了
3)在进行quicksort,得到两边区间再放进去;
4)按理说,我们进行每一次的出栈元素的时候,每一次找基准(得到的是privot),划分出来的都是两段区间,我们按理说都是要把这两段区间都要放到栈里面,但是如果说最后找基准找出了一个这样的结果
5)6,8 ,7 ,9 ,10此时我们的得到的privot的值是9,那么我们就不需要将privot+1,end这段区间入栈,也就是说如果privot<end-1或者privot>start+1;才进行入栈操作;
6)所以说我们每一次进行pop操作的时候,都要把两端区间放到栈里面,至少基准左边或右边有两个元素以上才可以入栈
7)因为一个数据已经是有序的呀,再加上基准的数据,这两个数据一定是有序的呀;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;
import java.util.Stack;
class jajj {
public static int quicksort(int low,int high,int[] arr1)
{ int temp=arr1[low];
while(low<high)
{ if(low<high&&arr1[high]>=temp)
{
high--;
}
arr1[low]=arr1[high];
if(low<high&&arr1[low]<=temp)
{
low++;
}
arr1[high]=arr1[low];
}
arr1[low]=temp;
return low;
}
public static void main(String[] args) {
int arr1[]={101,90,78,100};
int low=0;
int high=arr1.length-1;
int middle=quicksort(0,arr1.length-1,arr1);
Stack<Integer> stack=new Stack<>();//我们的栈里面存放的是下标
//没有进入到循环,说明是在第一次放
if(middle>low+1)
{//满足这个条件说明左边至少有两个元素
stack.push(0);
stack.push(middle-1);
}
if(middle<high-1)
{//满足这个条件说明右边至少有两个元素
stack.push(middle+1);
stack.push(arr1.length-1);
}
while(!stack.empty())
{
int end=stack.pop();
int start=stack.pop();
middle=quicksort(start,end,arr1);
if(middle>start+1)//这里的start和end要和去除的元素相对应(上面的两个int)
{
stack.push(start);
stack.push(middle-1);
}
if(middle<end-1)
{
stack.push(middle+1);
stack.push(end);
}
}
System.out.println(Arrays.toString(arr1));
}
}
测试数据:5 1 2 6 9 7 10 8
归并排序(难点)写这个代码时,先分解,在合并;
1)我们在这里面要注意一下,我们的进行分解的终止条件是low>=high,也就是说low==high也是满足条件的,此时就不应该进行分解了
2)况且我们还需要进行注意,我们在进行分解的过程中,区间范围应该是
merge(array,left,mid)
merge(array,mid+1,right)
3)我们是不应该写成这样子的:merge(array,left,mid-1),merge(array,mid+1,right)
我们再从下到上进行合并,就是每一层合并一个个有序数组
合并两个从小到大有序数组
1)我们定义一个s1下标,从数组的arr1的第一个位置开始,我们定义一个e1下标,表示第一个数组的结束位置;
2)我们定义一个s2下标,从数组的arr2的第一个位置开始,我们定义一个e2下标,表示第二个数组的结束位置;
3)开辟一个新的数组,我们可以先让s1向后走,与s2的下标位置进行比较,谁小就把谁放到新开辟的数组下标里面,同时只想数据指向的这个引用下标向后进行++操作;同时新开辟的数组下标也进行++操作;
4)如果此时循环退出了,肯定是一个数组走完了,另一个数组没有走完,那么我们就把一个数组的所以值全部拷贝过来;
5)循环的条件是两个数组起始下标的值要小于末尾下标的值(s1<=e1,s2<=e2)
6)三个数组的名称要写对;
public static int[] MerageArray(int[] arr1,int[] arr2){ int[] array=new int[arr1.length+arr2.length]; int s1=0; int s2=0; int k=0; int e1=arr1.length-1; int e2=arr2.length-1; while(s1<=e1&&s2<=e2) { if(arr1[s1]<=arr2[s2]) { array[k]=arr1[s1]; s1++; k++; }else{ array[k]=arr2[s2]; s2++; k++; } } while(s1<=e1) { array[k]=arr1[s1]; s1++; k++; } while(s2<=e2) { array[k]=arr2[s2]; s2++; k++; } return array; } }
10 6 7 1 要考虑什么时候进行分解,什么时候回退,自己画图理解一下:
1)我们一定要理解当我们给定一段数据,low下标等于这段数据的头,high下标指定这段数据的尾,每一次我们进行分解的时候,先算中间位置的下标
2)然后再从low到中间位置进行分解,从中间位置到high在进行分解,循环往复,向下递归
3)分解成一个一个的元素之后,在进行向上回退,合并有序数组
其实本质上来说:我们要进行归并排序的最终思想是把一个数组分成一个个单个的元素,在进行两个两个合并,四个四个合并
分解的思想:
1)我们再进行递归排序的时候,有一个头下标(head),中间下标(mid),末尾下表(tail);
我们每一次进行向下进行分解
head=0,tail=array.length()-1;
mid=(head+tail)/2
2)我们此时再针对low和high,以及mid+1,high再次进行分解,我们划分这两段区间进行分割
左边:
high=mid;
mid=(low+high)/2;
右边:
low=mid+1;
mid=(low+high)/2;
3)我们说最终的目的是拆分成一个个的数据,直到这一层的每一个部分的low和high进行相遇,说明不用再次进行拆分;
1 6 7 10 2 3 4 9
进行合并的时候arr1[low+i]=arr2[i];
我们假设可以想一想:现在有一个数组,一共有8个元素,向下分解一次,每一组四个元素,然后直接合并,尤其是在合并的时候的思想
public static void hesort(int low,int high,int middle,int[]arr1)
{
int s1=low;
int e1=middle;
int s2=middle+1;
int e2=high;
int k=0;//记录数组的下标
int arr2[]=new int[high-low+1];//合并临时数组temp的大小
while(s1<=e1&&s2<=e2)//这个过程是合并两个数组
{
if(arr1[s1]<=arr1[s2])
{
arr2[k]=arr1[s1];
s1++;
k++;
}
else{
arr2[k]=arr1[s2];
s2++;
k++;
}
}
while(s1<=e1)//说明第一个数组中还有元素
{
arr2[k]=arr1[s1];
k++;
s1++;
}
while(s2<=e2)//说明第二个数组中还有元素
{
arr2[k]=arr1[s2];
k++;
s2++;
}
for(int i=0;i<arr2.length;i++)
{
arr1[i+low]=arr2[i];
//把新创建出的数组元素拷贝到原来的数组当中,左边的数据,和右边的数据拷贝是不一样的,如果说不加上low那么直接进行覆盖原来前面已经排好序的元素
}
}
public static void sort(int arr1[],int low,int high)
{
if(low>=high) return;//这是分解的条件
int middle=(low+high)/2;
sort(arr1,low,middle);//递归分解数组的左边
sort(arr1,middle+1,high);//递归分解数组的右边
hesort(low,high,middle,arr1);
//这是每一行合并的过程low middle middle+1 high
}
}
时间复杂度:每一层都要合并,一共有logn层,每一层都要操作N个数据,所以时间复杂度为N*logN;
空间复杂度为:每一层都要开辟一个数组,O(N)
是一个稳定的排序,array[s1]<array[e1]比较的时候不取等于号,那么就是不稳定的排序;
稳定的排序:冒泡,归并,直接插入排序;
4)非递归实现归并排序:
非递归排序的代码本质上要和递归排序的代码要一样,先画出递归展开图
1)根据归并排序递归的思想来实现非递归方面的代码:上面进行归并排序的时候,先是两个两个有序,再是四个四个有序,先进行合并两个有序数组(两个有序数组的元素个数都是1);
2)就是把这个数组每一次分成了大小不同的若干个组,让我们每一次分成的每一组有序
3)假设数组一共有8个元素,我们可以先分成每一组从一个数据,再分成每一组两个数据,再分成每一组四个数据,这就是说模拟递归中归的过程
让他们每一组的数组分别进行有序,最后一组的个数是数组中的元素的个数;
4)每一组有一个元素的时候,i要向后遍历一次,每一组有两个元素的时候,i要向后遍历一次,每组一个数据的时候,我们要合并两个组
1)非递归实现归并排序只需要完成归并操作即可 , 一一归并,两两归并,四四归并,直至整体有序 , 排序完成。
2)对数组进行分组,每组元素为gap,初始时为1,按照2组为一个单位进行有序合并,合并后每组元素为gap=2*gap,直到每组元素个数大于或等于排序数组元素个数len为止
3)合并过程中需要保证调整后的mid与right不能越界,如果越界需要调整为待排序序列的最后一个元素下标
现在我们每一组有两个元素,我们来看一下是如何进行分组的:
现在我们来看一下每一组有四个元素的情况
每一次left的值都是i下标的值
两个元素中间永远是gap-1个元素
1--->2--->4-->8
public static void sort(int low,int high,int middle,int[]arr1)
{
int s1=low;
int e1=middle;
int s2=middle+1;
int e2=high;
int k=0;//记录数组的下标
int arr2[]=new int[high-low+1];
while(s1<=e1&&s2<=e2)//这个过程是合并两个数组
{
if(arr1[s1]<=arr1[s2])
{
arr2[k]=arr1[s1];
s1++;
k++;
}
else{
arr2[k]=arr1[s2];
s2++;
k++;
}
}
while(s1<=e1)//说明第一个数组中还有元素
{
arr2[k]=arr1[s1];
k++;
s1++;
}
while(s2<=e2)//说明第二个数组中还有元素
{
arr2[k]=arr1[s2];
k++;
s2++;
}
for(int i=0;i<arr2.length;i++)
{
arr1[i+low]=arr2[i];//把新创建出的数组元素拷贝到原来的数组当中,左边的数据,和右边的数据拷贝是不一样的;
}
}
public static void mergesort(int[] array)
{
int gap=1;//这表示每一组的数据个数,每经过一次遍历之后,分成组的个数要增加,是原来的2倍
while(gap<array.length)
{
//每当我们重新确定每一组的个数时,都要重新遍历原数组,从i下标开始向后进行遍历
for(int i=0;i< array.length;i+=gap*2){
int left=i;
int mid=i+gap-1;
if(mid>= array.length)
{
mid= array.length-1;
}
int right=mid+gap;
if(right>= array.length)
{
right= array.length-1;
}
sort(left,right,mid,array);
}
gap=gap*2;//每一次我们在不断增大组数
}
}
}
5)海量数据的排序问题
内部排序:指的是在内存上面进行排序,我们在idea上面写的数组,其实都是在我的内存上面
外部排序:排序过程中需要在磁盘等外部存储进行的排序
举一个场景:前提是内存中只可以存放1G,但是实际上有100G的数据需要进行排序
不可以将100G全部加载到你的内存里面进行运行的,内存不可以全部用完,只用来进行执行你的排序操作
但是归并排序是最常见的外部排序
因为此时在内存中我们无法将所有的数据全部放进去,所以我们需要外部排序来进行
1)我们可以根据100G数据把文件分成200份,每一份512M(把100G大文件分成200份)
2)分别在内存里面对512M文件里面的数据进行排序,此时这个文件的数据在内存里面,所以说运用任何排序算法都是可以的(现在内存中进行排序),然后我再将这些数据从内存中写到这个文件里面,每一个文件都这么做,以此类推,这样200个文件每一个文件就都有序了
我们让每一个小文件有序
3)同时对200文件进行归并,对200份内存数据进行归并(两个文件两个文件进行归并,相当于是两个有序数组进行合并,先让一个指针指向一个文件的数据,再让另一个指针指向另一个文件的数据,进行比较,谁大,就把这个数据放到一个新文件里面),新文件同时也就有序了;
4)直到把所有文件合并成一个大文件
6.计数排序
1)时间复杂度是O(N),空间复杂度和数据的范围是有关系的
2)当前代码是不稳定的,本质上稳定的,除非在借助一个数组
3)它本质上最适用于有N个数据,数据大小范围在0-N之间
例如现在有一组数2,5,3,0,2,3,0,3;
有了原数组之后,我们再次申请一个计数数组,长度为5;
我们遍历原来的数组
新的数组下标: 0 1 2 3 4 5
新的数组中的元素:2 0 2 3 0 1
1)我们统计每一个元素出现的次数,计数数组下标就是我们具体的数字,里面存放的就是我们这个数据在原数组中出现的次数
2)最后我们进行循环打印的时候,只要数组里面的元素不是0,那么我们就循环打印出现的次数;
计数数组:下标的值是原数据,里面的值是出现的次数
1)如何确定计数数组的大小?
2)假设现在有一个范围的数据:900-999,我们要开辟一个长度为1000的数组吗?如果这么做,就会浪费大量的空间,但是实际上这个数组应该是100就够了
4)要找到原来最小值和最大值,新的数组容量是maxVal-minVal+1;
5)计数数组的下标是array[i]-minval
时间复杂度:O(N)
空间复杂度:O(M),M表示数据的范围
稳定性:不稳定·
计数排序的步骤:(数据范围是900-999)
1)求数组的最大值MAX和最小值MIN;(999-900+1=100)
2)确定数组的长度MAX-MIN+1;
3)Count[array[i]-min]=array[i]:假设要存放923,那么对应计数数组的下标就是923-900=23下标
public static void counting(int[] array)
{
int maxval=array[0];
int minval=array[0];
for(int i=0;i<array.length;i++)
{
if(array[i]>maxval)
{
maxval=array[i];
}
if(array[i]<minval)
{
minval=array[i];
}
}
//此时已经找到了最大值和最小值
int[] CountArray=new int[maxval-minval+1];
for(int i=0;i<array.length;i++)
{
int index=array[i]-minval;
CountArray[index]++;
}
int indexArray=0;
说明此时在计数数组中,已经把原数组中每一个元素出现的个数全部记录了下来
接下来只需要遍历计数数组,把数据写回到array
for(int j=0;j<CountArray.length;j++)
{
while(CountArray[j]>0)
{
array[indexArray]=j+minval;
CountArray[j]--;//拷贝一个,次数也就少一个
indexArray++;
}
}
}