排序: 使一串记录按照其中的某个或者某些关键字的大小,递增或递减的排列起来的操作。(在以下文章中,通常指排升序)
说明: 排序是基于比较的排序,从小到大的排序,这篇博客详解了七个排序,但并不代表排序只有七种排法。
稳定性: 两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,则我们称该算法是具备稳定性的排序算法。【也可理解为发生跳跃式变换(即不在相邻的数之间变换)的就是不稳定的】
时间复杂度: 两个并列的时间复杂度相加(取较大的那个),两个嵌套的时间复杂度相乘。
空间复杂度: 若没有增加额外的空间,那么空间复杂度就是O(1)。
1、直接插入排序
(1)思路: 直接插入排序数组的第一个元素默认是有序的,所以要从1号下标开始比较,先把1号下标的元素放进临时空间,将临时空间的元素与有序数组的最后一个元素进行比较,如果有序数组中的元素大,就把该元素放进空着的位置,再把临时空间的元素放进刚刚有序数组中移动了元素的位置;如果有序数组的最后一个元素比临时空间的元素小,就继续把临时空间的元素放回原来的位置,然后i++,一直循环直到无序数组的最后一个元素排好序。
(2)时间复杂度: 最坏情况:O(n²);最好情况(有序情况):O(n)—— 越有序越快
(3)空间复杂度: O(1)
(4)是否稳定: if(array[j]>temp) 稳定;if(array[j]>=temp) 不稳定——>总结:稳定
如果一个排序是稳定的排序,那么它可以变为不稳定的排序;但是如果一个排序本身就是不稳定的,那么它是不可能变成一个稳定的排序的。如果是稳定的排序,那么在比较的过程中没有跳跃式的变换
import java.util.Arrays;
public class Sort {
public static void insertSort(int[] array){
for(int i=1;i<array.length;i++){
// 有序区间:[0,i)
// 无序区间:[i,array.length)
int temp=array[i];//先将无序区间的第一个数先放进临时空间temp里
int j;
for(j=i-1;j>=0;j--){
if(array[j]>temp){//然后将temp与有序空间的最后一个数进行比较
array[j+1]=array[j];//若temp小于有序空间当前下标的数,就将有序空间的下标往前移,直到找到使temp找到合适的位置插进去
}else{
break;//必须有的,不然当temp需要插入到有序数组的中间时,这层循环结束不了
}
}
array[j+1]=temp;//当j<0时跳出循环执行这一步或者若temp小于有序空间当前下标的数,就将temp赋值给j+1下标对应的位置
}
}
public static void main(String[] args) {
int[] array={10,5,8,4,1,9};
long start=System.currentTimeMillis();
System.out.println("排序前:"+Arrays.toString(array));//这样打印数组不用写循环
insertSort(array);
System.out.println("排序后:"+Arrays.toString(array));
long end=System.currentTimeMillis();
System.out.println("所用时间:"+(end-start));
}
}
//折半插入排序
public static void bsInsertSort(int[] array) {
for (int i = 1; i < array.length; i++) {
int v = array[i];
int left = 0;
int right = i;
// [left, right)
// 需要考虑稳定性
while (left < right) {
int m = (left + right) / 2;
if (v >= array[m]) {
left = m + 1;
} else {
right = m;
}
}
// 搬移
for (int j = i; j > left; j--) {
array[j] = array[j - 1];
}
array[left] = v;
}
}
2、希尔排序
希尔排序法又称缩小增量法,是对直接插入排序的优化。
(1)基本思想: 先选定一个数,把待排序文件中所有记录分成组,对每一组的数据进行排序,然后重复上述分组和排序的工作,直到所有的数据都排完序。(采用分组的思想,组内进行直接插入排序)
【如下图所示:对第一行的图进行一次循环解释:根据直接插入排序的思想,同一种颜色所对应的数据为同一组数据,拿红色线对应的数据来说,这一组里面对应的间隔是5,所以12对应的是j下标,8对应的是i下标,然后将8放进temp里,再使temp与j下标对应的数据进行比较,比较大的放在后面的位置,即把12放在i下标所对应的位置,然后j-5发现小于0,就把8放在j+5所对应的下标的位置(即12原先所处的位置),然后i++,i下标到了33那个位置,j=i-5,所以j下标在5那个位置,把33放进temp,使temp和j下标所对应的数据比较,谁大谁放在靠后的位置。。。一直到循环结束,这是结束了将数据分为5组的一次循环,接下来将数据分别分成3组和1组的进行比较,发现会很快的使数据有序。】
(2)时间复杂度: 最坏情况:O(n²);最好情况:O(n)
(3)空间复杂度: O(1)
(4)是否稳定: 不稳定(发生跳跃式变换)
import java.util.Arrays;
public class Sort {
public static void shellSort(int[] array){//用这个函数使数组里的数据分组排序
int[] drr={5,3,1};
for(int i=0;i<drr.length;i++) {
shell(array, drr[i]);//将该drr数组传进shell排序函数中,第一次分5组,第二次分3组,第三次分1组
}
}
//该排序的详解在直接插入排序的讲解里就有,不再赘述,过程一样一样的!
public static void shell(int[] array,int gap){//这里的排序函数就是直接插入排序,但是需注意i和j开始的地方(见上图),j减的时候应该是j=j-gap
for(int i=gap;i<array.length;i++){
int temp=array[i];
int j;
for(j=i-gap;j>=0;j-=gap){
if(array[j]>temp){
array[j+gap]=array[j];//这里发生跳跃式变换
}else{
break;
}
}
array[j+gap]=temp;
}
}
//其实结果就相当于,在shellSort那个函数里,将怎么分组用数组表示出来,
// 然后使真正的排序算法在分组的循环中将每种分组形式都过一遍,然后顺序就排出来了,真神奇呢!
public static void main(String[] args) {
int[] array={12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
long start=System.currentTimeMillis();
System.out.println("排序前:"+Arrays.toString(array));//这样打印数组不用写循环
shellSort(array);
System.out.println("排序后:"+Arrays.toString(array));
long end=System.currentTimeMillis();
System.out.println("所用时间:"+(end-start));
}
}
1、直接选择排序
(1)基本原理:每一次从无序区间选出最大(或最小)的一个元素,存放在无序区间的最后(或最前),直到全部待排序的数据元素排完。
(2)时间复杂度: O(n²)
(3)空间复杂度: O(1)
(4)是否稳定: 不稳定(发生跳跃式变换)
import java.util.Arrays;
public class Sort {
public static void selectSort(int[] array){//这些代码就是图片上的字面意思,没有什么好详解的
for(int i=0;i<array.length;i++){
for(int j=i+1;j<array.length;j++){
if(array[i]>array[j]) {//这里极大可能发生跳跃式变换
int temp = array[j];
array[j] = array[i];
array[i] = temp;
}
}
}
}
public static void main(String[] args) {
int[] array={12,4,5,10,3};
long start=System.currentTimeMillis();
System.out.println("排序前:"+Arrays.toString(array));//这样打印数组不用写循环
selectSort(array);
System.out.println("排序后:"+Arrays.toString(array));
long end=System.currentTimeMillis();
System.out.println("所用时间:"+(end-start));
}
}
(1)思路: 从小到大排序应该建立的是大根堆(若是建立小根堆,只知道最顶端元素是最小的,其余的元素无法知道其排序),最顶端的元素肯定是最大的,每次将顶端元素和下标为end的元素(先开始使最后一位元素的下标为end)交换,交换完之后要向下调整,使end–,再使第二(三、四、五、、、)次调整后的堆顶元素与end下标所对应的位置进行交换,一直循环,直至end=0,这时候的堆就是从小到大的排序了
(2)时间复杂度: O(nlog2ⁿ)
(3)空间复杂度: O(1)
(4)是否稳定: 不稳定(发生跳跃式变换)
import java.util.Arrays;
public class Sort {
//堆排序:先建堆,将建成的堆调整为大根堆,使堆顶元素与下标为end的元素(从堆的最后一个元素走起)进行交换,交换完end--,再调整,直至end<0
public static void createHeap(int[] array){//建堆
for(int i=(array.length-1-1)/2;i>=0;i--){//从最后一棵子树开始调整,i表示的是父结点
adjustDown(array,i,array.length);
}
}
//root:每棵子树开始的位置;len:结束位置
public static void adjustDown(int[] array,int root,int len){//向下调整
int parent=root;
int child=2*parent+1;
while(child<len){//最起码有左孩子
if(child+1<len&&array[child]<array[child+1]){//确保有右孩子并且右孩子>左孩子,所以需要调整的孩子结点要变成右孩子
child++;
}
if(array[child]>array[parent]){
int temp=array[child];
array[child]=array[parent];
array[parent]=temp;
parent=child;
child=2*parent+1;
}else{
break;
}
}
}
public static void heapSort(int[] array){
createHeap(array);
int end=array.length-1;
while(end>0){//根结点和end下标对应的结点进行交换
int temp=array[0];
array[0]=array[end];
array[end]=temp;
adjustDown(array,0,end);
end--;
}
}
public static void main(String[] args) {
int[] array={27,15,19,18,28,34,65,49,25,37};
long start=System.currentTimeMillis();
System.out.println("排序前:"+Arrays.toString(array));//这样打印数组不用写循环
heapSort(array);//堆排序
System.out.println("排序后:"+Arrays.toString(array));
long end=System.currentTimeMillis();
System.out.println("所用时间:"+(end-start));
}
}
1、冒泡排序
(1)思路: 使相邻的两个数进行比较,谁大谁放在后面。每一趟比较完最大的就在最后。
注意: 弄个语句判断是否发生交换,是因为可以减少最外层的循环,万一数组本来就是有序的,就不用非得等到最外层循环结束再结束,就可以直接return,提高效率。
(2)时间复杂度: O(n²),优化之后(已有序)可能达到O(n)
(3)空间复杂度: O(1)
(4)是否稳定: 稳定(未发生跳跃式变换)
import java.util.Arrays;
public class Sort {
public static void bubbleSort(int[] array){
for(int i=0;i<array.length-1;i++){//i表示趟数
boolean flg=false;//趟数优化需要
for(int j=0;j<array.length-1-i;j++){//区间优化
if(array[j]>array[j+1]){
int temp=array[j];
array[j]=array[j+1];
array[j+1]=temp;
flg=true;//搁这儿弄个语句判断是否发生交换
//若发生交换那flg=true,若没发生交换,则这个if语句都不用进来,
// 待循环结束就直接到了if(flg==false)这里了,能到这里说明数组已经有序
}
}
if(flg==false){
return;
}
}
}
public static void main(String[] args) {
int[] array={7,4,8,2,6};
long start=System.currentTimeMillis();
System.out.println("排序前:"+Arrays.toString(array));//这样打印数组不用写循环
bubbleSort(array);
System.out.println("排序后:"+Arrays.toString(array));
long end=System.currentTimeMillis();
System.out.println("所用时间:"+(end-start));
}
}
(1)思路: 从待排序区间,选一个数作为基准值,遍历整个待排序区间,比这个基准值小的放在这个数的左边,比这个基准值大的放到这个数的右边。然后再缩小范围,继续选一个基准值,把大的放在基准值右边,小的放在基准值左边,直到数组有序。
(2)时间复杂度: 最好情况:O(nlog2ⁿ);最坏情况(逆序):O(n²)
快排要快,那么每次划分序列的时候,如果都可以均匀的进行划分,那么效率最好。
(3)空间复杂度: 最好情况:O(log2ⁿ);最坏情况:O(n)
(4)是否稳定: 不稳定(发生跳跃式变换)
递归:
import java.util.Arrays;
public class Sort {
public static void insert_Sort(int[] array,int start,int end){//优化方法一需要调用的直接插入排序算法
for(int i=start+1;i<=end;i++){
int temp=array[i];//先将无序区间的第一个数先放进临时空间temp里
int j;
for(j=i-1;j>=start;j--){
if(array[j]>temp){//然后将temp与有序空间的最后一个数进行比较
array[j+1]=array[j];//若temp小于有序空间当前下标的数,就将有序空间的下标往前移,直到找到使temp找到合适的位置插进去
}else{
break;//必须有的,不然当temp需要插入到有序数组的中间时,这层循环结束不了
}
}
array[j+1]=temp;//当j<0时跳出循环执行这一步或者若temp小于有序空间当前下标的数,就将temp赋值给j+1下标对应的位置
}
}
public static void three_num_mid(int[] array,int left,int right){//优化方法二需要调用的函数。三数取中,提高效率
//array[mid]<=array[left]<=array[right];
int mid=(left+right)/2;
if(array[left]>array[right]){
int temp=array[left];
array[left]=array[right];
array[right]=temp;
}
if(array[mid]>array[left]){
int temp=array[left];
array[left]=array[mid];
array[mid]=temp;
}
if(array[mid]>array[right]){
int temp=array[mid];
array[mid]=array[right];
array[right]=temp;
}
}
public static int partition(int[] array,int low,int high){//找基准值
int temp=array[low];
while(low<high){
while((low<high)&&(array[high]>temp)){
high--;
}
array[low]=array[high];
while((low<high)&&(array[low]<temp)){
low++;
}
array[high]=array[low];
}
array[low]=temp;
return low;
}
public static void quick(int[] array,int left,int right){//规定每次排序的是哪些范围
if(left>right){
return;
}
//优化方式一:当待排序序列的数据个数小于等于100(随便给的范围)的时候,用直接插入排序
if(right-left+1<100){//当数据趋于有序时,这一步优化可以减小时间复杂度
insert_Sort(array,left,right);//insert_Sort()这个函数只应用于这里
return;
}
//优化方法二:
//three_num_mid(array,left,right);//三数取中,three_num_mid()这个函数只应用到这里
int par=partition(array,left,right);
quick(array,left,par-1);
quick(array,par+1,right);
}
public static void quickSort(int[] array){
quick(array,0,array.length-1);
}
public static void main(String[] args) {
int[] array={6,1,2,7,9,3,4,5,10,8};
long start=System.currentTimeMillis();
System.out.println("排序前:"+Arrays.toString(array));//这样打印数组不用写循环
quickSort(array);//快速排序
System.out.println("排序后:"+Arrays.toString(array));
long end=System.currentTimeMillis();
System.out.println("所用时间:"+(end-start));
}
}
非递归:
思路: 先找到基准值,然后将基准值的左右的最前和最后的下标都入栈,比如第一次找到的基准值是下标为5的元素,那么就该把下标0,4,6,9入栈,然后找数对,依次弹出两个栈顶元素,弹出的这两个元素构成一个区间,然后在这个区间里继续找基准值(找基准值的过程实际也是排序的过程),当栈不为空时就重复以上的操作,直到所有的元素都有序(这个时候栈应该为空)。
写程序的顺序:建栈,将left和right赋初值,找第一次的基准值,将基准值左右两侧的最前和最后下标入栈,当栈不为空时,一次出两个栈顶元素,分别使其成为right和left,再找一次基准值,再将基准值的左右的最前和最后下标入栈,直到栈为空。
import java.util.Arrays;
import java.util.Stack;
public class Sort {
public static int partition(int[] array,int low,int high){//找基准值
int temp=array[low];
while(low<high){
while((low<high)&&(array[high]>temp)){
high--;
}
array[low]=array[high];
while((low<high)&&(array[low]<temp)){
low++;
}
array[high]=array[low];
}
array[low]=temp;
return low;
}
//写程序的顺序:建栈,将left和right赋初值,找第一次的基准值,将基准值左右两侧的最前和最后下标入栈,
//当栈不为空时,一次出两个栈顶元素,分别使其成为right和left,再找一次基准值,再将基准值的左右的最前和最后下标入栈,直到栈为空
public static void quickSort(int[] array){
Stack<Integer> stack=new Stack<>();//存放数对
int left=0;
int right=array.length-1;
int par = partition(array, left, right);
if (par > left + 1) {//将基准值左边下标入栈
stack.push(left);
stack.push(par-1);
}
if (par < right - 1) {//将基准值右边下标入栈
stack.push(par+1);
stack.push(right);
}
while(!stack.empty()) {
right=stack.pop();//将left和right重新赋值
left=stack.pop();
par = partition(array, left, right);//继续找基准值
if (par > left + 1) {//将基准值左边下标入栈
stack.push(left);
stack.push(par-1);
}
if (par < right - 1) {//将基准值右边下标入栈
stack.push(par+1);
stack.push(right);
}
}
}
public static void main(String[] args) {
int[] array={6,1,2,7,9,3,4,5,10,8};
long start=System.currentTimeMillis();
System.out.println("排序前:"+Arrays.toString(array));//这样打印数组不用写循环
quickSort(array);//快速排序
System.out.println("排序后:"+Arrays.toString(array));
long end=System.currentTimeMillis();
System.out.println("所用时间:"+(end-start));
}
}
1、归并排序
(1)思路: 将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
(2)时间复杂度: O(nlog2ⁿ)
(3)空间复杂度: O(n)
(4)是否稳定: 稳定(可实现为一个不稳定的排序)。在合并时只是进入到另外一个数组里了,并没有发生跳跃式交换
递归:
import java.util.Arrays;
public class Sort {
public static void mergeSortInternal(int[] array,int low,int high){//拆分
if(low>=high){
return;
}
//分解
int mid=(low+high)>>>1;
mergeSortInternal(array,low,mid);//将一系列数拆分成一个一个的
mergeSortInternal(array,mid+1,high);
//合并
merge(array,low,mid,high);
}
public static void merge(int[] array,int low,int mid,int high){//归并两个有序段
int s1=low;
int s2=mid+1;
int len=high-low+1;//ret的数组长度
int i=0;//用来表示ret的下标
int[] ret=new int[len];//用来存放归并后的数组
while(s1<=mid && s2<=high){//在规定的范围里,依次判断s1和s2的大小,然后再拿出来放在ret数组里
if(array[s1]<=array[s2]){
ret[i++]=array[s1++];
}else{
ret[i++]=array[s2++];
}
}
while(s1<=mid){//若s2中没有元素,s1中还有元素那么将s1中剩余的元素拿出来
ret[i++]=array[s1++];
}
while(s2<=high){//若s1中没有元素,s2中还有元素那么将s2中剩余的元素拿出来
ret[i++]=array[s2++];
}
for(int j=0;j<ret.length;j++){//这里需要注意!!!,不能按原位置从ret复制到array
array[j+low]=ret[j];
}
}
public static void mergeSort(int[] array){
mergeSortInternal(array,0,array.length-1);
}
public static void main(String[] args) {
int[] array={10,6,7,1,3,9,4,2};
long start=System.currentTimeMillis();
System.out.println("排序前:"+Arrays.toString(array));//这样打印数组不用写循环
mergeSort(array);//归并排序
System.out.println("排序后:"+Arrays.toString(array));
long end=System.currentTimeMillis();
System.out.println("所用时间:"+(end-start));
}
}
思路: 如图。总的来说就是使两个归并段 (先使每个归并段只有一个元素,然后2倍的增长) 进行比较,谁小,谁先拿出来放在结果数组里,然后不断扩展归并段的规模,直到数据全部进入一个归并段,其中有很多细节需要考虑,比如每个归并段的范围、临界条件、只剩一个归并段怎么办、各个变量的初值和结束位置、某个需要判断的地方需要不需要=等等,认认真真的思考!!!【只是图和文字看不懂的话就结合代码,代码的注释部分有些地方说的很详细。】
import java.util.Arrays;
public class Sort {
public static void mergeSort(int[] array){
for(int gap=1;gap<array.length;gap*=2){//规定每组划分多少个数据
mergeNor(array,gap);
}
}
public static void mergeNor(int[] array,int gap){
int[] ret=new int[array.length];//存放结果的数组
int k=0;//ret的下标
int s1=0; //先确定4个位置
int e1=s1+gap-1;
int s2=e1+1;
int e2=s2+gap-1 < array.length ? s2+gap-1 : array.length-1;//e2不能越界,最大只能等于array.length-1
while(s2<array.length){//1、够两个归并段
//2、对应的s1位置和s2位置进行比较,谁小先把谁拿出来
while(s1<=e1 && s2<=e2) {
if (array[s1] <= array[s2]) {
ret[k++] = array[s1++];
}else {
ret[k++] = array[s2++];
}
}
//3、在比较完之后,肯定先有一个先走完,判断是谁没走完,需要把剩下的数据拷贝到结果数组当中
/*if(s1>e1 && s2<=e2){//这一段和下面的两个while看似是对等的,实际上是不对的,因为不能保证还有剩余元素的归并段只剩下一个元素,用while就可以解决这个问题
ret[k++]=array[s2++];
}
if(s2>e2 && s1<=e1){
ret[k++]=array[s1++];
}
*/
while(s1<=e1){
ret[k++]=array[s1++];
}
while(s2<=e2){
ret[k++]=array[s2++];
}
//4、接着确定新的s1 e1 s2 e2
s1=e2+1;
e1=s1+gap-1;
s2=e1+1;
e2=s2+gap-1 < array.length ? s2+gap-1 : array.length-1;//e2不能越界,最大只能等于array.length-1
}
//5、还需判断是否有另外的单一的归并段,若是有另外的单一的归并段,则需将它拷贝到ret的后面
//有两种情况:一种是这个单一的归并段有两个及以上的元素;另一种是这个单一的归并段只有一个元素,
//所以判断条件不能使s1<=e1,因为e1都可能越界,所以只要s1
while(s1<array.length){
ret[k++]=array[s1++];
}
for(int i=0;i<array.length;i++){//然后将元素从ret数组搬到array数组
array[i]=ret[i];
}
}
public static void main(String[] args) {
int[] array={15,2,35,6,23,11,5};
long start=System.currentTimeMillis();
System.out.println("排序前:"+Arrays.toString(array));//这样打印数组不用写循环
mergeSort(array);//归并排序
System.out.println("排序后:"+Arrays.toString(array));
long end=System.currentTimeMillis();
System.out.println("所用时间:"+(end-start));
}
}
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
- 先把文件切分成 200 份,每个 512 M
- 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
- 进行 200 路归并,同时对 200 份有序文件做归并过程,最终结果就有序了
计数排序
基数排序