排序算法
直接插入排序:
将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。
时间复杂度:O(n^2)
稳定的
1 public class InsertSort { 2 3 public static void insertSort(int[] arr){ 4 int len = arr.length ; 5 if (arr == null || len == 0) 6 return ; 7 for(int i = 1 ; i < len ; i++){ 8 if (arr[i] < arr[i-1]){ 9 int t = arr[i] ; //待插入的 10 int j = i-1 ; 11 while(j >= 0 && t < arr[j]){ 12 arr[j+1] = arr[j] ; 13 j-- ; 14 } 15 arr[j+1] = t ; 16 } 17 } 18 } 19 20 public static void main(String[] args) { 21 int[] arr = {3,1,5,7,2,4,9,6,5}; 22 insertSort(arr) ; 23 System.out.println(Arrays.toString(arr)); 24 } 25 }
希尔排序:
先将要排序的一组记录按某个增量d(n/2,n为要排序数的个数)分成若干组子序列,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。继续不断缩小增量直至为1,最后使用直接插入排序完成排序。
时间复杂度:O(nlogn)
不稳定的
1 public class ShellSort { 2 3 public static void shellInsertSort(int[] arr , int len , int dk){ 4 for(int i = dk ; i < len ; i++){ 5 if (arr[i] < arr[i-dk]){ 6 int t = arr[i] ; //待插入的 7 int j = i-dk ; 8 while(j >= 0 && t < arr[j]){ 9 arr[j+dk] = arr[j] ; 10 j-=dk ; 11 } 12 arr[j+dk] = t ; 13 } 14 } 15 } 16 17 public static void shellSort(int[] arr ){ 18 int len = arr.length ; 19 if (arr == null || len == 0) 20 return ; 21 int dk = len/2 ; 22 while(dk >= 1){ 23 shellInsertSort(arr,len,dk) ; 24 dk /= 2; 25 } 26 } 27 28 public static void main(String[] args) { 29 int[] arr = {3,1,5,7,2,4,9,6,5}; 30 shellSort(arr) ; 31 System.out.println(Arrays.toString(arr)); 32 } 33 }
选择排序:
在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
时间复杂度:O(n^2)
不稳定的
1 public class SelectSort { 2 3 public static void selectSort(int[] arr){ 4 int len = arr.length ; 5 if (arr == null || len == 0) 6 return ; 7 for(int i = 0 ; i < len-1 ; i++){ 8 int min = i ; 9 for(int j = i+1 ; j < len ; j++){ 10 if (arr[j] < arr[min]){ 11 min = j ; 12 } 13 } 14 if(min != i){ 15 int t = arr[i] ; 16 arr[i] = arr[min] ; 17 arr[min] = t ; 18 } 19 } 20 } 21 22 public static void main(String[] args) { 23 int[] arr = {3,1,5,7,2,4,9,6,5}; 24 selectSort(arr) ; 25 System.out.println(Arrays.toString(arr)); 26 } 27 }
堆排序:
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均大于等于其子女的值,根结点(堆顶元素)的值是最大的。也称大根堆或大顶堆。
时间复杂度:O(nlogn)
不稳定的
1 public class HeapSort { 2 3 public static void heapAdjust(int[] arr , int len , int parent){ 4 int leftChild = 2*parent+1 ; 5 int rightChild = 2*parent+2 ; 6 int maxIndex = parent ; 7 if(parent <= (len-1)/2){ 8 if (leftChild < len && arr[leftChild] > arr[maxIndex]) 9 maxIndex = leftChild ; 10 if (rightChild < len && arr[rightChild] > arr[maxIndex]) 11 maxIndex = rightChild ; 12 if (maxIndex != parent){ 13 int t = arr[maxIndex] ; 14 arr[maxIndex] = arr[parent] ; 15 arr[parent] = t ; 16 heapAdjust(arr,len,maxIndex) ; 17 } 18 } 19 } 20 public static void heapBuild(int[] arr , int len){ 21 for(int i = (len-1)/2 ; i >= 0 ; i--) //从第一个非叶子节点开始 22 heapAdjust(arr,len,i) ; 23 } 24 public static void heapSort(int[] arr){ 25 int len = arr.length ; 26 if (arr == null || len == 0) 27 return ; 28 heapBuild(arr,len) ; 29 for(int i = len-1 ; i >= 0 ; i--){ 30 int t = arr[0] ; 31 arr[0] = arr[i] ; 32 arr[i] = t ; 33 heapAdjust(arr,i,0) ; 34 } 35 } 36 public static void main(String[] args) { 37 int[] arr = {3,1,5,7,2,4,9,6,5}; 38 heapSort(arr) ; 39 System.out.println(Arrays.toString(arr)); 40 } 41 }
冒泡排序:
在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
时间复杂度:O(n^2)
不稳定的
1 public class BubbleSort { 2 3 public static void bubbleSort(int[] arr){ 4 int len = arr.length ; 5 if (arr == null || len == 0) 6 return ; 7 for(int i = 0 ; i < len-1 ; i++){ 8 for(int j = 0 ; j < len-1-i ; j++){ 9 if (arr[j] > arr[j+1]){ 10 int t = arr[j] ; 11 arr[j] = arr[j+1] ; 12 arr[j+1] = t ; 13 } 14 } 15 } 16 } 17 18 public static void main(String[] args) { 19 int[] arr = {3,1,5,7,2,4,9,6,5}; 20 bubbleSort(arr) ; 21 System.out.println(Arrays.toString(arr)); 22 } 23 }
快速排序:
1)选择一个基准元素
2)通过一趟排序讲待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大。
3)此时基准元素在其排好序后的正确位置
4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。
时间复杂度:最优或平均O(nlogn)
最差的情况,数组基本有序,此时会退化为冒泡排序,O(n^2)
空间复杂度:最优 O(logn) 最差O(n)
不稳定的
1 public class QuickSort { 2 3 public static int partition(int[] arr , int left , int right){ 4 int len = arr.length ; 5 if (arr == null || len == 0 || left < 0 || right >= len) 6 return -1; 7 int k = left ; 8 for(int i = left ; i < right ; i++){ 9 if (arr[i] < arr[right]){ 10 if (i != k){ 11 int t = arr[k] ; 12 arr[k] = arr[i] ; 13 arr[i] = t ; 14 } 15 k++ ; 16 } 17 } 18 int t = arr[k] ; 19 arr[k] = arr[right] ; 20 arr[right] = t ; 21 return k ; 22 } 23 24 public static void quickSort(int[] arr , int left , int right){ 25 if (left == right) 26 return ; 27 int index = partition(arr,left,right) ; 28 if (index > left) 29 quickSort(arr,left,index-1) ; 30 if (index < right) 31 quickSort(arr,index+1,right) ; 32 } 33 34 public static void main(String[] args) { 35 int[] arr = {3,1,5,7,2,4,9,6,5}; 36 quickSort(arr,0,arr.length-1) ; 37 System.out.println(Arrays.toString(arr)); 38 } 39 }
选择基准的方式
对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。
最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列
我们介绍三种选择基准的方法
方法(1):固定位置
思想:取序列的第一个或最后一个元素作为基准
如果输入序列是随机的,处理时间可以接受的。如果数组已经有序时,此时的分割就是一个非常不好的分割。因为每次划分只能使待排序序列减一,此时为最坏情况,快速排序沦为起泡排序,时间复杂度为Θ(n^2)。而且,输入的数据是有序或部分有序的情况是相当常见的。因此,使用第一个元素作为枢纽元是非常糟糕的,为了避免这个情况,就引入了下面两个获取基准的方法。
方法(2):随机选取基准
引入的原因:在待排序列是部分有序时,固定选取枢轴使快排效率底下,要缓解这种情况,就引入了随机选取枢轴
思想:取待排序列中任意一个元素作为基准
:这是一种相对安全的策略。由于枢轴的位置是随机的,那么产生的分割也不会总是会出现劣质的分割。在整个数组数字全相等时,仍然是最坏情况,时间复杂度是O(n^2)。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。
方法(3):三数取中(median-of-three)
引入的原因:虽然随机选取枢轴时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取枢轴
分析:最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为枢纽元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。显然使用三数中值分割法消除了预排序输入的不好情形,并且减少快排大约14%的比较次数
举例:待排序序列为:8 1 4 9 6 3 5 2 7 0
左边为:8,右边为0,中间为6.
我们这里取三个数排序后,中间那个数作为枢轴,则枢轴为6
1 /*函数作用:取待排序序列中low、mid、high三个位置上数据,选取他们中间的那个数据作为枢轴*/ 2 int SelectPivotMedianOfThree(int arr[],int low,int high) 3 { 4 int mid = low + ((high - low) >> 1);//计算数组中间的元素的下标 5 6 //使用三数取中法选择枢轴 7 if (arr[mid] > arr[high])//目标: arr[mid] <= arr[high] 8 { 9 swap(arr[mid],arr[high]); 10 } 11 if (arr[low] > arr[high])//目标: arr[low] <= arr[high] 12 { 13 swap(arr[low],arr[high]); 14 } 15 if (arr[mid] > arr[low]) //目标: arr[low] >= arr[mid] 16 { 17 swap(arr[mid],arr[low]); 18 } 19 //此时,arr[mid] <= arr[low] <= arr[high] 20 return arr[low]; 21 //low的位置上保存这三个位置中间的值 22 //分割时可以直接使用low位置的元素作为枢轴,而不用改变分割函数了 23 }
优化1、当待排序序列的长度分割到一定大小后,使用插入排序。
优化2、在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割
归并排序:
归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
时间复杂度:O(nlogn)
空间复杂度:O(n)
稳定的
1 public class MergeSort { 2 3 public static void merge(int[] arr , int left1 , int right1 , int left2 , int right2){ 4 int i = left1 ; 5 int j = left2 ; 6 int[] t = new int[right2-left1+1] ; 7 int k = 0 ; 8 while(i <= right1 && j <= right2){ 9 if (arr[i] < arr[j]) 10 t[k++] = arr[i++] ; 11 else 12 t[k++] = arr[j++] ; 13 } 14 while(i <= right1) 15 t[k++] = arr[i++] ; 16 while(j <= right2) 17 t[k++] = arr[j++] ; 18 for(int m = 0 ; m < t.length ; m++){ 19 arr[left1+m] = t[m] ; 20 } 21 } 22 23 public static void mergeSort(int[] arr , int left , int right){ 24 if (left < right){ 25 int mid = (right - left)/2 + left ; 26 mergeSort(arr,left ,mid) ; 27 mergeSort(arr,mid+1,right) ; 28 merge(arr,left,mid,mid+1,right) ; 29 } 30 } 31 32 public static void main(String[] args) { 33 int[] arr = {3,1,5,7,2,4,9,6,5}; 34 mergeSort(arr,0,arr.length-1) ; 35 System.out.println(Arrays.toString(arr)); 36 } 37 }