博主整理了十大排序算法于此篇博文,从原理到代码实现。
必背的表
没错,这是一张必背的表。不过我们可以理解着来背。
时间复杂度一定要记住平均时间复杂度。
至少要记住插入排序、堆排序、归并排序、快速排序。
注:不稳:两个相等的数,有可能在排序之后的顺序发生变化。
空间复杂度:不需要额外空间则为1。
验证算法:肉眼不准确,我们可以采用对数器。
static int[] randomArray(){
Random r=new Random();
int[] arr=new int[10000];
for(int i=0;i<arr.length;i++){
//10000以内的随机数
arr[i]=r.nextInt(10000);
}
return arr;
}
static void check(){
//一定要拷贝一个新数组,不然永远比较一样的数组就没有意义了
int[] arr1=randomArray();
int[] arr2=new int[arr1.length];
//public static native void arraycopy(Object src, int srcPos, Object dest, int destPos,int length);
// src:要复制的数组(源数组) srcPos:复制源数组的起始位置 dest:目标数组 destPos:目标数组的下标位置 length:要复制的长度
System.arraycopy(arr1,0,arr2,0,arr1.length);
Arrays.sort(arr1);
//要验证的自己写的算法
xuanze.sort(arr2);
boolean flag=true;
for(int i=0;i<arr1.length;i++){
if(arr1[i]!=arr2[i]){
flag=false;
break;
}
}
System.out.println(flag);
}
选择排序
思想最简单的排序。
从头到尾,找到最大(最小)的值的下标,和第一位交换;下一轮从第二到尾,找到次大(次小)的值的下标,和第二位交换……一直到找到倒数第二位确定,完毕。
举例:5 2 1 3 4->(交换下标0和2的数字) 1 2 5 3 4-> 1 2 5 3 4->(交换下标2和3的数字) 1 2 3 5 4-> (交换下标3和4的数字)1 2 3 4 5
import java.util.Scanner;
public class xuanze {
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=sc.nextInt();
}
//从下标0比较到下标n-2
for(int i=0;i<n-1;i++){
int min=i;
for(int j=i+1;j<n;j++){
min=arr[j]<arr[min]?j:min;
}
swap(arr,i,min);
}
print(arr);
}
static void print(int[] arr){
for(int i=0;i<arr.length;i++){
System.out.print(arr[i]+" ");
}
}
static void swap(int[] arr,int i,int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
冒泡排序
从第一个数开始往后两两比较,大的放在后面,一次可以把最大放在最后面。 每一轮都比较到最后一个尚未排序的位置。(像一个泡泡冒到最右边)
举例:8 6 3 4 7-> 6 8 3 4 7-> 6 3 8 4 7->6 3 4 8 7-> 6 3 4 7 8(第一轮完成)->3 6 4 7 8(这时只比较到7)->3 4 6 7 8->3 4 6 7 8(第二轮完成)->3 4 6 7 8-> 3 4 6 7 8(第三轮完成)->3 4 6 7 8(第四轮完成,完毕)
import java.util.Scanner;
public class maopao {
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=sc.nextInt();
}
//两两比较,所以比较n-1次
for(int i=0;i<n-1;i++){
//n-1-i后面都排好序了(冒好泡了)
for(int j=0;j<n-1-i;j++){
if(arr[j]>arr[j+1]){
swap(arr,j,j+1);
}
}
}
}
static void print(int[] arr){
for(int i=0;i<arr.length;i++){
System.out.print(arr[i]+" ");
}
}
static void swap(int[] arr,int i,int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
插入排序
适合用于样本小,基本有序的数组。
每次抽出第i个数,逐个跟前面的数比较。
举例:87123- >78123(第一轮)->71823->17823(第二轮)->17283->12783(第三轮)->12738->12378(完毕)
import java.util.Scanner;
public class charu {
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=sc.nextInt();
}
//从第二个(下标为1)的数字开始和前面的数字比较
for(int i=1;i<n;i++){
for(int j=i;j>0;j--){
if(arr[j]<arr[j-1]){
swap(arr,j,j-1);
}
}
}
print(arr);
}
static void print(int[] arr){
for(int i=0;i<arr.length;i++){
System.out.print(arr[i]+" ");
}
}
static void swap(int[] arr,int i,int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
希尔排序
前面选择、冒泡、插入是简单排序,接下来七种要稍微难一点。
希尔排序是改进的插入排序。
先看一个例子:假如一个值gap=4,则0 4 8 12等下标的数先插入排序排好序;接下来1 5 9 13等下标的数排序……一共四组排序。然后gap=2再排一遍,最后一定要有gap=1再排一遍。(等下再说gap怎么来的)
希尔排序为何比插入排序快?插入排序时,起始位置和最终位置间隔比较大的数(比如数字1,起始下标在20)需要跟前面的数比较很多次(20、19、……、1)才能挪到排序后的位置,但是希尔排序就可以减少很多次排序(20、16、12、……)迅速挪到相应的位置;并且,起始位置和最终位置间隔比较小的数,移动的距离短(和前面的好处相映相成)。
import java.util.Scanner;
public class shellsort {
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=sc.nextInt();
}
//希尔最初的想法就是gap二分
//gap =n/2……4 2 1(最后一定要到1)
for(int gap=n/2;gap>0;gap/=2){
//这里为什么是i++?因为4的位置要比较,5、6……都需要比较,所以i++
for(int i=gap;i<n;i++){
//j>gap-1,因为不能越界;j每次减去gap
for(int j=i;j>gap-1;j-=gap){
if(arr[j]<arr[j-gap]){
swap(arr,j,j-gap);
}
}
}
}
print(arr);
}
static void print(int[] arr){
for(int i=0;i<arr.length;i++){
System.out.print(arr[i]+" ");
}
}
static void swap(int[] arr,int i,int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
后来出现了Knuth序列,认为是更高效的。
h=1;h=3*h+1;(h<=n/3)
改进后的代码部分:
int h=1;
while(h<=n/3){
h=h*3+1;
}
for(int gap=h;gap>0;gap=(gap-1)/3){
for(int i=gap;i<n;i++){
for(int j=i;j>gap-1;j-=gap){
if(arr[j]<arr[j-gap]){
swap(arr,j,j-gap);
}
}
}
}
归并排序
涉及到递归的思想,暂时不引申。
数组排序->子数组排序->子数组的子数组排序->……->一直到最后只剩两个数,排序,递归返回,完毕。
Java和Python中的对象排序用的就是改进的归并排序。 (对象排序要求稳定,为什么呢?除了数值这个属性,其他属性可能不一样,排序如果不稳定,会把两个数值一样但并不一样的两个对象弄混)
两个已经有序的数组要进行排序,怎么排呢?(假如1 4 6 7 10,还有2 3 5 8 9)
额外开辟一个等长(10)的数组。准备三个指针,i指向第一个数组开始的位置(数值1),j指向第二个数组开始的位置(数值2),k指向新开辟的数组开始的位置。i和j指向的值开始比较,小的赋值给k指向的位置,小的那个指针后移,k后移,直到排序完毕。(一个数组完毕后,另一个数组剩下的数可以直接放在新数组后面)
import java.util.Scanner;
public class shellsort {
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=sc.nextInt();
}
sort(arr,0,n-1);
print(arr);
}
private static void sort(int[] arr,int left,int right){
//跳出递归条件
if(left==right){return;}
//分成两半
int mid=left+(right-left)/2;
//左边排序
sort(arr,left,mid);
//右边排序
sort(arr,mid+1,right);
//两两合并
merge(arr,left,mid,right);
}
private static void merge(int[] arr,int left,int mid,int right){
int i = left;
int j = mid+1;
int[] temp=new int[right-left+1];
int k = 0;
while (i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[k++] = arr[i++];
}else {
temp[k++] = arr[j++];
}
}
while(i<=mid){
temp[k++] = arr[i++];
}
while(j<=right){
temp[k++] = arr[j++];
}
//排序后在temp里,要把temp的数放进arr里
for(int m=0;m<temp.length;m++){
arr[left+m]=temp[m];
}
}
static void print(int[] arr){
for(int i=0;i<arr.length;i++){
System.out.print(arr[i]+" ");
}
}
}
Timsort:多路归并(改进的归并)。
快速排序
最常考的排序之一。
定一个基准,比基准小的在左边,比基准大的在右边。
import java.util.Scanner;
public class quicksort {
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=sc.nextInt();
}
sort(arr,0,n-1);
print(arr);
}
private static void sort(int[] arr,int low,int high){
if(low>=high){return;}
int index=partition(arr,low,high);
sort(arr,low,index-1);
sort(arr,index+1,high);
}
private static int partition(int[] arr,int low,int high){
int pivot=arr[low];
int left=low;
int right=high;
while (left<right){
//基准在左边的第一个数,就先移动right指针,这样最后交换low和left的位置才能找到对的数
while (left<right&&arr[right]>pivot){
right--;
}
//考虑到重复元素,这里用的是<=
while (left<right&&arr[left]<=pivot){
left++;
}
if(left<right){
swap(arr,left,right);
}
}
swap(arr,low,left);
return left;
}
static void print(int[] arr){
for(int i=0;i<arr.length;i++){
System.out.print(arr[i]+" ");
}
}
static void swap(int[] arr,int i,int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
当数组基本有序,每次取第一个值当基准值,则partition会重复n次,快排退化成O(n^2)。解决办法其一为采用随机基准快排,只需改变一点点代码:(随机数组一个数作为基准,和第一个数交换,别的都一样)
private static int partition(int[] arr,int low,int high){
//int pivot=arr[low];
int p=(int)(low+Math.random()*(high-low+1));
int pivot=arr[p];
swap(arr,low,p);
堆排序
堆排序会涉及到二叉树的内容,暂时不引申。
计数排序
非比较排序,桶思想的一种特殊情况。相对用的多一些。
适用于量很大,但是数取值范围比较小。
新建数组,长度为取值范围的差(比如0~60,取61为长度)。
遍历题目的数组,每次下标相应符合,对应数字+1。输出的时候,从头开始每一位输出对应数字个的数。
import java.util.Arrays;
import java.util.Scanner;
public class jishu1 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// int n = sc.nextInt();
// int[] arr = new int[n];
// for (int i = 0; i < n; i++) {
// arr[i] = sc.nextInt();
// }
int[] arr={100,101,105,103,107,104,102,109,109,107,106,106,106,105,104,107,108,109,103,105,106,105,101,102,100,102,109,108,107};
sort(arr);
}
private static void sort(int[] arr){
int[] res=new int[arr.length];
//范围为100-109
//下面这是一个不稳定的算法
// int[] count=new int[10];
// for(int i=0;i<arr.length;i++){
// count[arr[i]-100]++;
// }
// for(int i=0,j=0;i<count.length;i++){
// while (count[i]-->0){res[j++]=i+100;}
// }
// System.out.print(Arrays.toString(res));
//那么稳定的是怎么样的呢
int[] count=new int[10];
for(int i=0;i<arr.length;i++){
count[arr[i]-100]++;
}
//累加数组,可以判断每一个重复数的最后一个数的下标
for(int i=1;i<count.length;i++){
count[i]+=count[i-1];
}
//对原来的数组进行倒序排列
for(int i=arr.length-1;i>=0;i--){
res[--count[arr[i]-100]]=arr[i];
}
System.out.print(Arrays.toString(res));
}
}
基数排序
非比较排序,桶思想的一种特殊情况。
多关键字排序。每一步都类似计数排序,有高位优先和低位优先。
桶排序
桶排序用的不多,因为桶的长度不好定。如果是数组ArrayList,排序好排,但扩容容易浪费,如果用链表,空间不浪费了,但排序费时间。 重点掌握还是计数排序和基数排序。
(日后继续会完善)