Java实现几大常用的排序算法
1,简单插入排序
主要思路将num[i]插入到一个已经排好的数组num[0…i-1]中 ,因为涉及到数据的查找和移动,最坏情况下平均时间复杂度为o(n^2).
public static void insertion_sorting(int[] nums){
int temp;
int j,i;
for (i = 1;i < nums.length; ++i){
if (nums[i] < nums[i-1]){
temp = nums[i];
for (j = i-1;j >= 0&&nums[j]>temp; --j)nums[j+1] = nums[j];
nums[j+1] = temp;
}
}
}
折半插入排序
插入排序的主要思想就是在一个有序的序列中不停的查找,移动。因此查找过程可以用折半查找代替。
public static void BInsertion_Sorting(int[] nums){
int temp;
for (int i = 1;i < nums.length; ++i) {
if (nums[i] < nums[i - 1]) {
temp = nums[i];
int low = 0, height = i - 1;
while (low <= height) {
int m = (low + height) / 2;
if (nums[m] <= temp) low = m + 1;
else height = m - 1;
}
for (int j = i - 1; j >= height + 1; --j) nums[j + 1] = nums[j];
nums[height + 1] = temp;
}
}
}
引入折半查找的方法后虽然可以减少查找的步骤,但是无法减少数据移动的步骤,因此折半插入排序时间复杂度依然是o(n^2)
2,希尔排序
希尔排序是一种改进的插入排序,因为排序时数组都有序度越高,插入排序的时间复杂度就越低,如果排序前数据已经是有序的,那么插入排序的时间复杂度就是o(n),如果是最坏情况,插入排序的时间复杂度就是o(n^2).
基于这个特点可以先将数组按照一定的规则分成几个部分,先对这几个部分进行初步的插入排序,然后再对整个部分进行插入排序,因为局部已经做到初步的“有序”,所以可以尽量的减少最后一个插入排序的时间复杂度。而划分部分的方法可以通过设定增量的方式进行。
//希尔排序,先将数组按照增量分成若干个小份,先对小份进行插入排序然后在对整个数组进行插入排序
//增量的选择视数组的情况而定
public static void Shell_sort(int[] nums){
int[] dlta = new int[]{5,3,1}; //增量序列
int temp,k;
for (int i = 0;i<dlta.length;++i){
int dk = dlta[i]; //保存增量
for (int j = dk;j<nums.length;j++){
if (nums[j]<nums[j-dk]){
temp = nums[j];
for (k = j-dk;k>=0&&nums[k]>temp;k-=dk)nums[k+dk] = nums[k];
nums[k+dk] = temp;
}
}
}
}
希尔排序的时间复杂度依赖于增量的选择,大致范围是O(nlogn)~O(n2)。
3,冒泡排序
通过“交换”思想进行的排序,通过相邻数字两两交换的方式将最大/小的数放到 最后,然后将次大/小的数放到第二的位置,以此类推。平均时间复杂度o(n^2).
public static void bubble_sort(int[] nums){
int temp;
for (int i = nums.length-1;i>0;--i){
for (int j = 0;j<i;++j){
if (nums[j]>nums[j+1]){
temp = nums[j+1];
nums[j+1] = nums[j];
nums[j] = temp;
}
}
}
}
4,快速排序
冒泡排序的改进版,通过一趟排序将记录分割成两部分,其中一部分记录的关键子均比另一部分的关键字小,然后将分别对这两部分记录继续排序,直到整个序列有序。
//快速排序
private static int Partition(int[] a,int low,int height){
int pivotkey = a[low];
while (low<height){
while (low<height&&a[height]>=pivotkey)height--;
a[low] = a[height];
while (low<height&&a[low]<=pivotkey)low++;
a[height] = a[low];
}
a[low] = pivotkey;
return low;
}
private static void QSort(int[] nums,int low,int height){
if (low<height){
int m = Partition(nums,low,height);
QSort(nums,low,m-1);
QSort(nums,m+1,height);
}
}
public static void QuickSort(int[] nums){
QSort(nums,0,nums.length-1);
}
首先选取记录中第一个元素作为枢轴,然后将小于它的放在其左边,大于它的放在其右边,然后再分别对两部分递归执行上述过程,直到整个记录有序。
快速排序平均时间复杂度o(nlogn),但在最坏情况下为o(n^2)
5,简单选择排序
主要思路就是将排行第几的数字放在排行第几的位置上,与冒泡排序很像,以及与我经常将他们搞混,但是实质上是有区别的。冒泡排序是在遍历的过程中不停的交换相邻的两个元素,将最大/小的元素移动到数组的最后,选择排序是先将最大/小的元素找出来,然后和对应位置的元素交换,只在每一趟的最后进行一次交换,突出一个"选择"二字。
//选择排序
public static void selection_sort(int[] nums){
int temp;
int i,j;
for (i=0;i<nums.length-1;++i){
int index = i;
for (j=i+1;j<nums.length;++j){
if (nums[i] > nums[j]&&nums[j]<nums[index]){
index = j;
}
}
temp = nums[i];
nums[i] = nums[index];
nums[index] = temp;
}
}
6,堆排序
上面的几种排序算法都是基于一种“比较"的思想,不管是“选择”还是”插入“,都是基于比较结果来进行后续操作,比如甲想要考全班第一,他需要赢过其他所有人,乙想考全班第二,他需要赢过除甲以外的所有人。但是事实上并不用如此,如果甲的成绩比乙高,乙的成绩比丙高,那么自动就可以认为甲的成绩比丙高,必须要再比一次。而堆排序就是基于这种”锦标赛"的思想,建立的。
首先将记录处理成一个堆(关于什么是堆这里不做讨论),之后堆的根节点就是最大/小的节点,然后将最后的元素跟根节点交换,将其再次处理成堆,循环往复,直到有序。
//堆排序,平均时间复杂度为o(nlog(n)),不推荐在数据数量较少的情况下使用,因为建堆和调整堆很费时间
public static void heapsort(int[] nums){
//将一个无序的数组调整成一个堆
for (int i=nums.length/2;i>=0;--i){
HeapAdjust(nums,i,nums.length);
}
//进行堆排序
for (int i=nums.length-1;i>0;--i){
int temp = nums[0];
nums[0] = nums[i];
nums[i] = temp;
HeapAdjust(nums,0,i);
}
}
//nums[] 保存当前要进行调整的堆,s表示堆的根节点,m表示堆的节点 数量
private static void HeapAdjust(int[] nums,int s, int m){
int temp = nums[s];
for (int i=s*2;i<m-1;i*=2){
if (nums[i] < nums[i + 1])i++;
if (nums[i]<=temp)break;
nums[s] = nums[i];
s = i;
}
nums[s] = temp;
}
平均时间复杂度为o(nlog(n))
7,归并排序
“归并"的含义是将两个或者两个以上的有序表组合成一个新的有序表。首先两两合并,然后合并后的序列在两两合并,直到最后两个大的子序列合并。
//归并排序
private static int[] newNums; //归并排序的辅助空间
public static void mergeSort(int[] nums){
//将辅助空间设定为与待排序列等大,其实不需要等大,在排序 过程中需要多大申请多大就行了,但是我懒省事
newNums = new int[nums.length];
//开始排序
MSort(nums,0,nums.length-1);
}
//
private static void MSort(int[] nums,int s,int t){
if (s != t){
int m = (s+t)/2; //获取数组中间位置
MSort(nums,s,m); //递归调用
MSort(nums,m+1,t); //递归调用
Merge(nums,s,m,t); //排序算法
}
}
private static void Merge(int nums[],int s,int m,int t){ //i开始,m中间,n结尾
//将需要排序的段复制到辅助空间中
for (int i = s;i<=t;++i){
newNums[i] = nums[i];
}
//以类似起扑克的方式将需要排序的段重新排序,并复制到原数组中
int i,j;
int k = s;
for (i = s,j = m+1;i<=m&&j<=t;){
if (newNums[i]>=newNums[j]){
nums[k] = newNums[j];
j++;
k++;
}else {
nums[k] = newNums[i];
i++;
k++;
}
}
if (i > m){
for (int n = j;n<=t;++n){
nums[k] = newNums[n];
k++;
}
}else {
for (int n = i;n<=m;++n){
nums[k] = newNums[n];
k++;
}
}
}
时间复杂度o(nlogn)
8,基数排序
基数排序和前面的排序都不相同,它通过链表的方式实现。首先向记录处理成链表,然后再创建出十个链表,对应’0-9’十个数字,然后获取记录中各数据个位上的数,按照上面创建的链表存入,然后将链表连起来,及将记录按照个位上的数进行了一次初排。之后在获取十位上的数,重复上述操作,知道所有位上的数都检查了一遍。
//基数排序
public static void radixSort(int[] nums){
//创建十个arrayList用于保存中间生成的list
ArrayList<Integer> list_0,list_1,list_2,list_3,list_4, list_5,
list_6,list_7,list_8,list_9;
//保存数组中的最大值
int maxNumber = 0;
//numList用于保存结果列表
ArrayList<Integer> numList = new ArrayList<>();
list_0 = new ArrayList<>();list_1 = new ArrayList<>();list_2 = new ArrayList<>();
list_3 = new ArrayList<>();list_4 = new ArrayList<>();list_5 = new ArrayList<>();
list_6 = new ArrayList<>();list_7 = new ArrayList<>();list_8 = new ArrayList<>();
list_9 = new ArrayList<>();
//查询数组中的最大值,并且将数组处理成静态列表
for (int i=0;i<nums.length;++i){
if (nums[i]>=maxNumber)maxNumber = nums[i];
numList.add(nums[i]);
}
//p用于辅助获取每个位上的数字
int p = 1;
for (;maxNumber>0; maxNumber/=10){
for (int i=0;i<numList.size();++i){
int temp = numList.get(i)/p;
//获得每个位上的数字,并保存到对应的arrayList中
if (temp%10 == 0) list_0.add(numList.get(i));
else if (temp%10 == 1) list_1.add(numList.get(i));
else if (temp%10 == 2) list_2.add(numList.get(i));
else if (temp%10 == 3) list_3.add(numList.get(i));
else if (temp%10 == 4) list_4.add(numList.get(i));
else if (temp%10 == 5) list_5.add(numList.get(i));
else if (temp%10 == 6) list_6.add(numList.get(i));
else if (temp%10 == 7) list_7.add(numList.get(i));
else if (temp%10 == 8) list_8.add(numList.get(i));
else if (temp%10 == 9) list_9.add(numList.get(i));
}
p *= 10;
//清除结果序列,并将初步排列的数字重新添加到结果序列中
numList.clear();
for (int i=0;i<list_0.size();++i)numList.add(list_0.get(i));
for (int i=0;i<list_1.size();++i)numList.add(list_1.get(i));
for (int i=0;i<list_2.size();++i)numList.add(list_2.get(i));
for (int i=0;i<list_3.size();++i)numList.add(list_3.get(i));
for (int i=0;i<list_4.size();++i)numList.add(list_4.get(i));
for (int i=0;i<list_5.size();++i)numList.add(list_5.get(i));
for (int i=0;i<list_6.size();++i)numList.add(list_6.get(i));
for (int i=0;i<list_7.size();++i)numList.add(list_7.get(i));
for (int i=0;i<list_8.size();++i)numList.add(list_8.get(i));
for (int i=0;i<list_9.size();++i)numList.add(list_9.get(i));
list_0.clear();list_1.clear();list_2.clear();list_3.clear();
list_4.clear();list_5.clear();list_6.clear();list_7.clear();
list_8.clear();list_9.clear();
}
//将排完序的序列重新改成数组,并返回
for (int i = 0;i<numList.size();++i){
nums[i] = numList.get(i);
}
}
基数排序的时间复杂度是o(d*n),最优的情况下甚至可以做到线性时间,但是同样具有局限性。毫无疑问这是一种那空间换时间的算法,需要耗费大量的辅助空间。适用于n很大,关键字较小的序列。是想一下,对{1,10000000002}这个序列排序,用普通排序很容易解决,但是用基数排序需要浪费大量的空间和时间。对于小数和负数的处理同样有些复杂。
9,计数排序
计数排序就是首先用记录中的最大值创建一个Count数组,该数组的空间与记录中最大值等大(最理想情况下是申请Max-Min+1个空间),该数组用于存放记录中的数出现了几次。比如说记录中出现了两个5,就在Count[4]的位置记为2。注意,记录中的值与Count数组中的下标一一对应,及关键字值为5就在Count[4]的位置上加一,直到对整个记录遍历了一遍。然后用计数数组Count还原原数组。及如果Count[4]上的值为2,就在结果数组中添加两个5。因为我们遍历Count数组是顺序遍历的,所以原数组就自动排序了。
//计数排序
public static void countingSort(int[] nums){
int maxNum=0,minNum=Integer.MAX_VALUE;
for (int i=0;i<nums.length;++i){
if (nums[i]>=maxNum)maxNum = nums[i];
if (nums[i]<=minNum)minNum = nums[i];
}
int[] newNums = new int[maxNum+1];
for (int i=0;i<nums.length;++i) newNums[nums[i]]++;
int p = 0;
for (int i=0;i<newNums.length;++i){
if (newNums[i]!=0){
for (int m=newNums[i];m>0;--m){
nums[p] = i;
p++;
}
}
}
}
但是第一次看到这个算法时被震撼到了,原来还可以这么简单,而且成功将排序算法的时间复杂度拉到了线性水平o(n+k)。但是后来仔细想想,该方法存在很多局限性,比如要求数组必须是正整数,如果存在负数和小数需要先对数据进行处理。而且和上面同样的问题,如何数组中的最大值和最小值跨度很大,会浪费很多不必要的空间。比如上面的例子对{1,1000000000002}排序,明明只有两个元素却需要至少创建一个1000000000002大小的Count数组。所以只适用于n很大,且关键字大小很集中的记录。