这个排序应该是大部分程序员刚学习代码接触的第一个排序算法,它的原理也很简单,相邻的元素进行比较,满足条件就进行交换,总共需要遍历n轮。
时间复杂度O(N^2)
空间复杂度O(1)
到n轮进行完之后,整体就会有序。
public staic void bubbleSort(int arr[]){
for(int i=0;i<arr.length;i++){
for(int j=0;j<arr.length-i-1;j++){
int temp=0;
if(arr[j]>arr[j+1]){ //满足条件就交换
temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
}
选择排序其实和冒泡排序一样都需要进行n轮的遍历,只不过不同于冒泡排序的是选择排序条件成立过后不是直接交换,而是对这个元素进行标记。
原理:对数组中的需要比较的第一个元素进行标记,然后进行遍历比较,后续元素满足条件就标记到这个元素上,直到遍历元素被遍历完,将被标记的元素和第一个元素进行交换。
时间复杂度O(N^2)
空间复杂度O(1)
到n轮进行完之后,整体就会有序。
public static void selectionSort(int[] arr){
for (int i=0;i<arr.length;i++){
int minIndex=i;//标记元素
for (int j=i;j<arr.length;j++){
minIndex=arr[minIndex]<arr[j]?minIndex:j;//满足条件就切换标记元素
}
int temp=arr[i];
arr[i]=arr[minIndex];
arr[minIndex]=temp;
}
}
原理:从数组中的一个位置往前看,相邻的两个元素如果满足排序要求的条件,就直接进行下一轮的遍历检查,如果不满足,就与前一个位置的元素进行交换,然后继续往前看,直到这个元0素之前所有元素都满足排序所要求的条件再进行下一轮检查。
某些情况可能比选择和冒泡排序时间复杂度更优,可以认为是时间复杂度O(N^2)最好的一种排序。
时间复杂度O(N^2)
空间复杂度O(1)
到n轮进行完之后,整体就会有序。
public static void insertionSort(int[] arr){
for (int i=0;i<arr.length;i++){
for (int j=i-1;j>=0&&arr[j]>arr[j+1];j--){
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
原理:一个数组排序,先给左边排好序,再给右边排好序,最后让整体有序。
排序运用了递归的方法,在整体有序的过程中运用了双指针。
时间复杂度为O(N*logN)
空间复杂度O(N)
左边排好序的数组与右边排好序的数组中的元素从各自的第一个元素开始两两比较,满足条件就向下边的辅助数组中放元素,指针向右移,继续比较,最后哪个数组有剩余的元素挨个放到辅助数组当中去。最后数组做到有序。
public static void mergeSort(int[] arr){
if(arr==null||arr.length<2){ //无效数组直接返回
return;
}
mergeSort(arr,0,arr.length-1);
}
private static void mergeSort(int[] arr, int l, int r) {
if (l==r){
return;
}
int mid=l+((r-l)>>1);
mergeSort(arr,l,mid); //递归
mergeSort(arr,mid+1,r); //递归
merge(arr,l,mid,r);
}
private static void merge(int[] arr, int l, int mid, int r) {
int p1=l;
int p2=mid+1;
int i=0;
int[] help=new int[r-l+1];
while (p1<=mid&&p2<=r){ //数组比较 下放过程
help[i++]=arr[p1]<=arr[p2]?arr[p1++]:arr[p2++];
}
//哪个数组有剩余就依次放入辅助数组中
while (p1<=mid){
help[i++]=arr[p1++];
}
while (p2<=r){
help[i++]=arr[p2++];
}
for (i=0;i<help.length;i++){
arr[l+i]=help[i];
}
}
原理:荷兰国旗思想,把数组分为了三个区域,小于这个数的放左边区域,等于这个数放中间区域,大于这个数的放右边区域,递归从而让数组整体有序。
在快速排序中,往往需要从数组中找个随机数来作为划分值。和归并排序一样都是使用了压栈(先入
后出)的思想。
public static void quickSort(int[] arr){
if (arr==null||arr.length<2){
return;
}
quickSort(arr,0,arr.length-1);
}
private static void quickSort(int[] arr, int l, int r) {
if (l<r){
swap(arr,(int)(l+(Math.random()*(r-l+1))),r); //从数组中随机选取一个作为划分值
int[] p=partition(arr,l,r); //返回左边界和右边界
quickSort(arr,l,p[0]-1);//递归
quickSort(arr,p[1]+1,r);//递归
}
}
private static int[] partition(int[] arr, int l, int r) {
int less=l-1;
int more=r;
while (l<more){
if (arr[l]<arr[r]){
swap(arr,++less,l++);
}
else if(arr[l]>arr[r]){
swap(arr,l,--more);
}
else {
l++;
}
}
swap(arr,more,r);
return new int[]{less+1,more};
}
堆结构分为大根堆(最大堆)和小根堆(最小堆),堆顶为这个堆的最大或最小值,在一个大根堆中,任何一个父节点的值,都大于或等于它左,右孩子节点的值。小根堆同理。利用堆的结构,可以进行排序。
原理:在把数组调整为堆结构之后,把堆顶与最后一个元素进行交换,然后把现在的最后一个元素“抛出”,然后在调整堆结构,调整完毕的堆顶元素就成为了原数组中的第二大的元素,然后重复刚才的操作,直到堆中的元素全部“抛出”。排序完成。
public static void heapSort(int[] arr){
if (arr==null||arr.length<2){
return;
}
for (int i=0;i<arr.length;i++){
heapInsert(arr,i); //把数组调整为大根堆
}
int heapSize=arr.length;
swap(arr,0,--heapSize); //交换最大值
while (heapSize>0){
heapify(arr,0,heapSize); //下沉操作 调整堆结构
swap(arr,0,--heapSize);
}
}
private static void heapInsert(int[] arr, int index) {
while (arr[index]>arr[(index-1)/2]){
swap(arr,index,(index-1)/2);
index=(index-1)/2;
}
}
private static void heapify(int[] arr, int index, int heapSize) {
int left=2*index+1;
while (left<heapSize){
int largest=left+1<heapSize&&arr[left+1]>arr[left]?left+1:left;
largest=arr[largest]>arr[index]?largest:index;
if (largest==index){
break;
}
swap(arr,index,largest);
index=largest;
left=2*index+1;
}
}
1、计数排序
2、基数排序
这两种排序都是基于桶排序思想下的排序,都是不基于比较的排序。
取一个合适长度的计数数组,把需要排序的数组中的元素依次统计在计数数组当中去(统计当前数有多少个),最后按照顺序输出。
private static void countSort(int[] arr) {
if(arr==null||arr.length<2){
return;
}
int max=Integer.MIN_VALUE;
int min=Integer.MAX_VALUE;
for (int i=0;i<arr.length;i++){
min=Math.min(min,arr[i]);
max=Math.max(max,arr[i]);
}
int[] count=new int[max-min+1]; //给计数数组合适长度
for (int i=0;i<arr.length;i++){
count[arr[i]-min]++; //统计
}
int i=0;
for (int j=0;j<count.length;j++){
while (count[j]-->0){
arr[i++]=j+min; //根据下标完成排序
}
}
}
基数排序是根据数组中最大数的位数来模拟的几次入桶出桶,同时也有个长度为10的计数器,来给每一次入桶计数,完成了几次的入桶出桶,数组整体就会有序。
private static void radixSort(int[] arr, int begin, int end, int digit) {
final int radix=10;
int i=0,j=0;
int[] bucket=new int[end-begin+1];
for (int d=1;d<=digit;d++){ //有多少位就进出几次
int[] count=new int[radix];
for(i=begin;i<=end;i++){
//count[0] 当前位(d位)是0的数字有多少个
//count[1] 当前位(d位)是0,1的数字有多少个
//count[2] 当前位(d位)是0,1,2的数字有多少个
//count[i] 当前位(d位)是(0-i)的数字有多少个
j=getDigit(arr[i],d);
count[j]++;
}
for (i=1;i<radix;i++){ //前缀和
count[i]=count[i]+count[i-1];
}
for (i=end;i>=begin;i--){ //入桶
j=getDigit(arr[i],d);
bucket[count[j]-1]=arr[i];
count[j]--;
}
for (i=begin,j=0;i<=end;i++,j++){ //出桶
arr[i]=bucket[j];
}
}
}
private static int getDigit(int i, int d) { //得到某位的数
return (i/((int) Math.pow(10, d-1))%10);
}
前六个排序都是基于比较的排序,后两个排序都是不基于比较的排序,需要有比较合适的数据状况才能使用,所以说这些不基于比较的排序使用范围还是很有限的。
基于比较的排序也都各有优缺,得看需求来选择。
时间复杂度 | 空间复杂度 | 排序稳定性 | |
---|---|---|---|
选择排序 | O(N^2) | O(1) | 不具备 |
冒泡排序 | O(N^2) | O(1) | 具备 |
插入排序 | O(N^2) | O(1) | 具备 |
归并排序 | O(N*logN) | O(N) | 具备 |
快排(一般排序选择) | O(N*logN) | O(logN) | 不具备 |
堆排 | O(N*logN) | O(1) | 不具备 |
计数排序 | O(N) | O(N) | 具备 |
基数排序 | O(N) | O(N) | 具备 |