排序的基本概念
在本章学习中,我们默认为从小到大的排序,如果想要从大到小的排序,我们改变符号即可
(1)排序的稳定性
谁先在系统中先提交,谁就应该在排序中排在前面;像上图中的12,要达到稳定的排序就应该把红色的12放在前面
(2)内部排序:数据元素全都放在内存中的排序
(3)外部排序:就是当我们的数据元素非常多的时候,内存上放不下,我们就放在磁盘上,然后在磁盘上进行排序
常见的排序算法
插入排序
直接插入排序
【1】时间复杂度:
==最好情况:O(N)==数据完全有序的时候1,2,3,4,5;相当于i走而j不走
==最坏情况:O(N^2)==数据完全逆序的时候 5,4,3,2,1
【2】==空间复杂度:O(1)==因为它没有另外开辟空间
【3】稳定性:稳定的排序
PS:一个本身就是稳定的排序,是可以实现为不稳定的排序的;但是相反一个本身就是不稳定的排序,是不可能实现为稳定的排序
import java.util.Arrays;
public class Sort {
public static void insertSort(int[] array){
for (int i = 1; i < array.length ; i++) {
int temp=array[i];//先拿出来,否则就会被覆盖掉
int j=i-1;
for (; j >=0 ; j--) {
if(temp<array[j]){
array[j+1]=array[j];
}else{
break;
}
}
array[j+1]=temp;//跳出整个循环之后执行这一条代码
}
}
public static void main(String[] args) {
Sort so=new Sort();
int[] array={2,5,7,1,8,9};
so.insertSort(array);
System.out.println(Arrays.toString(array));//此处的array是上面的array重新排序之后输出的
}
}
注意:break是跳出整个循环,而continue只是跳出当前循环,继续进行下一个循环
下面我们构建一个Test来测试顺序,倒序,随机顺序三种array的直接插入排序的耗时
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Random;
//我们分别给顺序,逆序还有无序的进行排序时间测试
public class Test {
public static void orderArray(int[] array) {
for (int i = 0; i < array.length; i++) {
array[i]=i;
}
}
public static void notOrderArray(int[] array){
for (int i = 0; i <array.length ; i++) {
array[i]= array.length-i;
}
}
public static void notOrderArrayRandom(int[] array){
Random random=new Random();
for (int i = 0; i < array.length; i++) {
array[i]=random.nextInt(10_0000);
}
}
public static void testInsertSort(int[] array){
int[] tmpArray= Arrays.copyOf(array,array.length);
long startTime=System.currentTimeMillis();
Sort.insertSort(tmpArray);
long endTime=System.currentTimeMillis();
System.out.println("插入排序的耗时:"+(endTime-startTime));
}
public static void main(String[] args) {
int[] array=new int[10_0000];
orderArray(array);//一个顺序的数组
//notOrderArray(array);一个倒序的数组
//notOrderArrayRandom(array);随机生成数组中元素的顺序
testInsertSort(array);
}
}
PS:为什么要在testInsertSort方法中进行数组的拷贝?
因为如果你不进行拷贝的话,那么你后面再测试倒序数组的耗时的时候,你用的还是顺序数组的已经排好顺序的数组,不能达到测试目的
希尔排序
希尔排序是对直接插入排序的优化,它采用跳跃式分组,可能会将更小的元素尽可能的往前放
【1】时间复杂度:n^1.3 ^- n^1.5
希尔排序的时间复杂度不是一个精确的值,我们目前只能得到一个范围
【2】空间复杂度:O(1)
【3】稳定性:不稳定排序
public static void shellSort(int[] array){
int gap= array.length;//gap的值应该为数组长度
while(gap>1){//因为当gap=1的时候我们就直接进行排序了
gap/=2;
shell(array,gap);
}
//shell(array,gap)为什么不用写这个呢,是因为我们把while循环里面走一遍
// 我们就会发现在循环里面已经走了gap=1的代码
}
public static void shell(int[] array,int gap) {
for (int i = gap; i < array.length; i++) {
int temp = array[i];
int j = i - gap;
for (; j >= 0; j -= gap) {
if (array[j] > temp) {
array[j + gap] = array[j];
} else {
break;
}
}
array[j + gap] = temp;
}
}
我们希尔排序是直接插入排序的优化,所以我们在写代码的时候就把直接插入排序中+1或者-1的地方,都改成+gap或者-gap
希尔排序的测试
public static void testShellSort(int[] array){
int[] tmpArray=Arrays.copyOf(array,array.length);
long startTime=System.currentTimeMillis();
Sort.shellSort(array);
long endTime=System.currentTimeMillis();
System.out.println("希尔排序的耗时:"+(endTime-startTime));
}
public static void main(String[] args) {
int[] array=new int[10_0000];
//orderArray(array);//一个顺序的数组
notOrderArray(array);//一个倒序的数组
//notOrderArrayRandom(array);随机生成数组中元素的顺序
testInsertSort(array);
testShellSort(array);
}
PS:希尔排序一般考的很少,但是也有概率会被考到
选择排序
选择排序
【1】时间复杂度:不管是最好还是最坏都是O(N)
【2】空间复杂度:O(1)
【3】稳定性:不稳定排序
以下这两种方法的时间复杂度是一样的,但是选择排序对于我们来说不太实用,所以不经常使用
选择排序代码1——方法1
public static void selectSort(int[] array){
for (int i = 0; i < array.length ; i++) {
int minIndex=i;
for (int j = i+1; j < array.length; j++) {
if(array[j]<array[minIndex]){
minIndex=j;
}
}
swap(array,minIndex,i);
}
}
public static void swap(int[] array,int i,int j){
int temp=array[i];
array[i]=array[j];
array[j]=temp;
}
选择排序代码2——方法2
public static void selectSort2(int[] array){
int left=0;
int right= array.length-1;
while(left<right){
int minIndex=left;
int maxIndex=left;
for (int i = left+1; i <=right ; i++) {
if(array[i]<array[minIndex]){
minIndex=i;
}
if(array[i]>array[maxIndex]){
maxIndex=i;
}
}
swap(array,left,minIndex);
//最大值就在第一个的位置,最小值的left和minIndex已经交换了
//所以最大值就被换走了,为了保证maxIndex一直指向最大值,所以让maxIndex也挪过去,即 maxIndex=minIndex;
if(maxIndex==left){
maxIndex=minIndex;
}
swap(array,maxIndex,right);
left++;
right--;
}
}
**题目练习:**使用选择排序对长度为100的数组进行排序,则比较的次数为4950
**解析:**如果有n个元素,则第一次比较次数: n - 1;第二次比较次数: n - 2…
第n - 1次比较次数: 1
所有如果n = 100,则比较次数的总和:99 + 98 + … + 1=4950次。
堆排序
【1】时间复杂度:O(N*logN)
【2】空间复杂度:O(1)
【3】稳定性:不稳定性
堆排序是我们目前说这几种排序方式中,时间复杂度最快的
代码展示:
public static void createBigHeap(int[] array){
for (int parent = (array.length-1-1)/2; parent >=0 ; parent--) {
shiftDown(array,parent,array.length);
}
}
public static void shiftDown(int[] array,int parent,int end){
int child=(parent*2)+1;
while(child<end){
//限制一下范围,不超过树的整体就好
if(child+1<end&&array[child]<array[child+1]){
child++;
}
if (array[child] > array[parent]) {
swap(array,child,parent);
parent=child;
child=parent*2+1;
}else{
break;
}
}
}
public static void heapSort(int[] array){
createBigHeap(array);
int end= array.length-1;
while(end>0){//只要没有到根节点的时候,我们都可以进行循环
swap(array,0,end);
shiftDown(array,0,end);//为什么这块取end,是因为在shiftDown方法中(child<end)
end--;
}
}
交换排序
冒泡排序
【1】时间复杂度:O(n^2),如果加了优化,最好情况为O(N)
【2】空间复杂度:O(1)
【3】稳定性:稳定
public static void bubbleSort(int[] array){
for (int i = 0; i < array.length-1 ; i++) {
boolean flg=false;
for (int j =0; j < array.length-1-i ; j++) {
if(array[j+1]<array[j]){
swap(array,j,j+1);
flg=true;
}
}
if(!flg){//这样如果你在前几次就排好序了,后面就不用重复进行了
return;
}
}
}
快速排序
【1】时间复杂度:一般情况下你就答这个:最好情况:O(N*logN) 满二叉树或者完全二叉树
最坏情况:O(N^2) 单分支的树
【2】空间复杂度:==最好情况:O(logN) == 满二叉树或者完全二叉树,相当于树的高度
最坏情况:O(N) 单分支的树
【3】稳定性:不稳定
【4】快速排序算法是基于分治法的一个排序算法。
1.Hoare法快速排序
代码展示:
public static void quickSort(int[] array){
quick(array,0,array.length-1);
}
private static void quick(int[] array,int start,int end){
if(start>=end) return;//左边一个结点或者一个结点也没有,说明已经到最后了
int pivot=partition(array,start,end);//找到那个中间的那个,左边的都比它小,右边的都比它大
quick(array,start,pivot-1);
quick(array,pivot+1,end);
}
private static int partition(int[] array,int left,int right){
int key=array[left];
int i=left;
while(left<right){
while(left<right&&array[right]>=key){
right--;
}
while(left<right&&array[left]<=key){
left++;
}
swap(array,left,right);
}
//当left和right相遇的时候,我们就结束 while循环,所以我们要将找到的这个key换到序列中间的位置
swap(array,i,left); // 换的是元素,不是下标
return left;
}
PS:Q1:为什么要先循环right,再循环left?
如图所示,要是我们要让left先循环,right后循环,那么到最后达pivot的时候,即left和right相遇的地方,我们退出循环,将6和9交换,但是我们就发现了问题,此时pivot=6,但是6的左边有了比它大的9,所以说在进行Hoare快排找pivot的时候,我们只能让right先走循环,然后left再走
Q2:为什么要在循环的时候对key取等号?
等号是一定要取的,因为要是不取等号的话,left和right的值要是等于key的话,它就会一直进不去循环,left和right的值一直改变不了
2. 挖坑法快速排序
我们一般使用快排的时候,一般使用挖坑法而不是Heare法
public static void quickSort2(int[] array){
quick(array,0,array.length-1);
}
private static void quick2(int[] array,int start,int end){
if(start>=end) return;//左边一个结点或者一个结点也没有,说明已经到最后了
int pivot=partition2(array,start,end);//找到那个中间的那个,左边的都比它小,右边的都比它大
quick(array,start,pivot-1);
quick(array,pivot+1,end);
}
private static int partition2(int[] array,int left,int right) {
int key = array[left];
while (left < right) {
while (left < right && array[right] >= key) {
right--;
}
array[left] = array[right];
while (left < right && array[left] <= key) {
left++ ;
}
array[right] = array[left];
}
array[left] = key;
return left;
}
3.前后指针法(拓展)
一般不会考到,我们了解即可
public static void quickSort3(int[] array){
quick3(array,0,array.length-1);
}
private static void quick3(int[] array,int start,int end){
if(start>=end) return;
int pivot=partition3(array,start,end);
partition3(array,start,pivot-1);
partition3(array,pivot+1,end);
}
private static int partition3(int[] array,int left,int right){
int prev=left;
int cur=left+1;
while(cur<=right){
if(array[cur]<array[left]&&array[++prev]!=array[cur]){
swap(array,cur,prev);
}
cur++;
}
swap(array,prev,left);
return prev;
}
因为这三种方法它得到的基准都不一样,所以在选择题中考到相关题目,我们就需要一个一个去测试,我们建议的测试顺序是:1.挖坑法 2.Heare法 3.前后指针法
4.快速排序优化
1.用三数取中法获得key
2.在小范围内使用插入排序,减少递归的次数
public static void quickSort4(int[] array){
quick3(array,0,array.length-1);
}
private static void quick4(int[] array,int start,int end){
if(start>=end) return;
//三数取中——可以降低空间复杂度
int index=midOfTree(array,start,end);
swap(array,index,start);//这样就可以保证start下标所指的元素是中间大的数
int pivot=partition3(array,start,end);
partition3(array,start,pivot-1);
partition3(array,pivot+1,end);
}
public static int midOfTree(int[] array,int left,int right){
int mid=(left+right)/2;
if(array[left]<array[right]){
if(array[mid]<array[left]){
return left;
}else if(array[mid]>array[right]){
return right;
}else{
return mid;
}
}else{
if(array[mid]>array[left]){
return left;
}else if(array[mid]<array[right]){
return right;
}else{
return mid;
}
}
}
//在小范围里面我们就可以采用直接插入排序
public static void insertSortRange(int[] array,int begin,int end){
for (int i = begin+1; i <=end ; i++) {
int temp=array[i];
int j=i-1;
for (; j>=0 ;j--) {
if(array[j]>temp){
array[j+1]=array[j];
}else{
break;//跳出该次循环
}
}
array[j+1]=temp;
}
}
快速排序非递归
快排的非递归是在模拟递归的过程,所以时间复杂度并没有本质的变化,但是没有递归,可以减少栈空间的开销。栈和队列都可以实现
//快速排序非递归_使用一个栈来实现非递归
private static int partition3(int[] array,int left,int right){
int prev=left;
int cur=left+1;
while(cur<=right){
if(array[cur]<array[left]&&array[++prev]!=array[cur]){
swap(array,cur,prev);
}
cur++;
}
swap(array,prev,left);
return prev;
}
public static void quickSortNor(int[] array){
Stack<Integer> stack=new Stack<>();
int left=0;
int right=array.length-1;
int pivot=partition3(array,left,right);
if(pivot-1>left){//说明它左边还有元素
stack.push(left);
stack.push(pivot-1);
}
if(pivot+1<right){
stack.push(pivot+1);
stack.push(right);
}
while(!stack.isEmpty()){
right=stack.pop();
left=stack.pop();
pivot=partition3(array,left,right);
if(pivot-1>left){//说明它左边还有元素
stack.push(left);
stack.push(pivot-1);
}
if(pivot+1<right){
stack.push(pivot+1);
stack.push(right);
}
}
}
归并排序
【1】时间复杂度:O(N*logN)
【2】空间复杂度:O(N)
【3】稳定性:稳定
归并排序的缺点是空间复杂度大,始终是O(n)
归并排序的普通实现(递归实现)
//递归实现
public static void mergeSortFunc(int[] array, int left,int right){
int mid=(left+right)/2;
if(left>=right) return;
mergeSortFunc(array,0,mid);
mergeSortFunc(array,mid+1,right);
//分解之后进行合并
merge(array,left,right,mid);
}
public static void merge(int[] array,int left,int right,int mid){
int s1=left;
int s2=mid+1;
int[] tmpArr=new int[right-left+1];
int k=0;
//确保两个区间都有数据
while(s1<=mid&&s2<=right){
if(array[s2]<=array[s1]){
tmpArr[k]=array[s2];
k++;
s2++;
}else{
tmpArr[k]=array[s1];
k++;
s1++;
}
}
//如果有一个区间的s走完了,但是另一个区间的还没走完,我们需要进行新一轮的循环来走其中的数据
while(s1<=mid){
tmpArr[k]=array[s1];
k++;
s1++;
}
while(s2<=right){
tmpArr[k]=array[s2];
k++;
s2++;
}
//把所有元素按从小到大的顺序走完以后,我们要进行合并了,我们利用一个新的数组中完成合并
for (int i = 0; i <tmpArr.length ; i++) {
array[i+left]=tmpArr[i];
}
}
递归排序非递归实现
public static void mergeSortNor(int[] array){
int gap=1;
while(gap<array.length){
for (int i = 0; i <array.length ; i+=2*gap) {
int left=i;
int mid=left+gap-1;
int right=mid+gap;
//为了防止mid和right越界
if(mid>= array.length){
mid=array.length-1;
}
if(right>=array.length-1){
right=array.length-1;
}
merge2(array,left,right,mid);
}
gap*=2;
}
}
public static int[] merge2(int[] array,int left,int right,int mid){
int s1=left;
int s2=mid+1;
int[] tmpArr=new int[right-left+1];
int k=0;
//确保两个区间都有数据
while(s1<=mid&&s2<=right){
if(array[s2]<=array[s1]){
tmpArr[k]=array[s2];
k++;
s2++;
}else{
tmpArr[k]=array[s1];
k++;
s1++;
}
}
while(s1<=mid){
tmpArr[k]=array[s1];
k++;
s1++;
}
while(s2<=right){
tmpArr[k]=array[s2];
k++;
s2++;
}
for (int i = 0; i <tmpArr.length ; i++) {
array[i+left]=tmpArr[i];
}
return array;
}
海量数据的排序问题
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有1G,需要排序的数据有100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
1.先把文件切分成200份,每个512M
2.分别对512M排序,因为内存已经可以放的下,所以任意排序方式都可以
3.进行2路归并,同时对200份有序文件做归并过程,最后就有序了(一轮2路合并结束之后,再次开始新的一伦2路合并,直至结束为止)
计数排序(了解)
【1】时间复杂度:O(N+范围)
【2】空间复杂度:O(范围)
【3】稳定性:稳定
计数排序的时间复杂度和空间复杂度与你给定的范围有关
//计数排序
public static void coutSort(int[] array){
int minVal=array[0];//是元素值
int maxVal=array[0];
//1.我们先找出这组数中的最大值和最小值
for (int i = 0; i < array.length-1 ; i++) {
if(array[i]<minVal){
minVal=array[i];
}
if(array[i]>maxVal){
array[i]=maxVal;
}
}
//2.创建一个新的计数数组,来存放数据
int[] count=new int[maxVal-minVal+1];
//3.遍历原来的数组,利用这个数组来计数,看每个元素出现了几次
for (int i = 0; i < array.length-1 ; i++) {
count[array[i]-minVal]++;
}
//4.遍历count数组,将元素写回array数组中
int index=0;
for (int i = 0; i < count.length-1 ; i++) {
while(count[i]>0){
array[index]=minVal+i;
index++;
count[i]--;
}
}
}
几种排序算法的知识点汇总
1.占用辅助空间:(即空间复杂度)
a>归并排序:N;
b>快排 最好情况:logN 最坏情况:N
c>希尔为1;
d>堆排为1;
e>直接插入排序:1;
f>选择排序:1
g>冒泡排序:1
2.时间复杂度:
a>归并排序:nlogn;
b>快排:最好情况:O(N*logN) ;最坏情况:O(N^2)
c>希尔为n^1.3 ^- n^1.5
d>堆排为nlogn;
e>直接插入排序:最好情况:O(N);最坏情况:O(N^2)
f>选择排序:不管是最好还是最坏都是O(N)
g>冒泡排序:O(n^2),如果加了优化,最好情况为O(N)
3.最坏时间复杂度不为O(N^2):
a>堆排序最坏时间复杂度为nlogn;
b>快排如果每次划分只有一半区间,则时间复杂度为n^2;
c>选择排序:时间复杂度始终为n^2;
d>插入排序:如果序列逆序,每次都需要移动元素,时间复杂度n^2;
4.使用场景:
a>快排:初始排序影响较大,有序是性能最差;
b>插入:接近有序,性能最好;
c>希尔:希尔是对插入排序的优化,这种优化在无序序列中才有明显的效果,如果序列接近有序,反而是插入最优
d>堆排序,归并排序,选择排序这些对队列初始顺序不敏感,所以对其算法的性能无影响
5.各个算法的稳定程度:
a>直接插入一般可以从前向后进行元素的插入,相同元素的相对位置可以不发生变化
b>归并也可以保证相对位置不变
c>冒泡排序在元素相同的情况下也可以不进行交互,也可以保证稳定
d>选择排序的思想是每次选出最值,放在已排序序列的末尾,如果最值有多个,而选出的为最后一个最值,会导致相对位置发生变化。当然选择排序也可以变成稳定的,只要 保证相同的值选择第一个就可以
**6.目前所学的稳定的排序:**插入排序,冒泡排序,归并排序
7.排序方法中,每一趟排序结束时都至少能够确定一个元素最终位置的方法是① 选择排序② 快速排序③堆排序