java排序总结
- 冒泡排序(BubbleSort)
- 选择排序(SelctionSort)
- 插入排序(Insertion Sort)
- 希尔排序(Shell Sort)
- 快速排序(Quicksort)
- 归并排序(Merge Sort)
- 堆排序(HeapSort)
- 基数排序(RadixSort)
排序算法 | 平均时间复杂度 | 空间复杂度 |
冒泡排序 | O(n2) | 1 |
选择排序 | O(n2) | 1 |
插入排序 | O(n2) | 1 |
希尔排序 | O(n1.5) | 1 |
快速排序 | O(N*logN) | logN |
归并排序 | O(N*logN) | n |
堆排序 | O(N*logN) | 1 |
基数排序 | O(k*n) | n+k |
冒泡排序(BubbleSort)
基本思想:两个数比较大小,较大的数下沉,较小的数冒起来。平均时间复杂度是O(n2)
基本不用,太慢
for (int i=0;i<num.length-1;i++){
boolean flag = false;
for (int j = 0;j<num.length-1-i;j++){
if (num[j]>num[j+1]){
int tmp = num[j];
num[j] = num[j+1];
num[j+1] = tmp;
flag = true;
}
}
if (!flag){
break;
}
}
选择排序(SelctionSort)
基本思想:在长度为N的无序数组中,首先假设第一个元素是最小的第一次遍历后边的n-1个数中,找到最小的数值与第一个元素进行比较判断是否进行交换;
第二次遍历n-2个数,找到最小的数值与第二个元素比较判断是否进行交换;第n-1次遍历,找到最小的数值与第n-1个元素交换,排序完成。
基本不用,不稳定
public void SelctionSort(int[] num){
int n = num.length;
int k=0,tmp=0;
for (int i=0;i<n;i++){
k=i;
for (int j=i+1;j<n;j++){
if (num[j]<num[k]){
k=j;
}
}
if(k!=i){
tmp = num[i];
num[i] = num[k];
num[k] = num[i];
}
}
}
插入排序(Insertion Sort)
基本思想:
在要排序的一组数中,从第一个开始假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。(想象自己在打斗地主,摸牌的时候顺便把扑克牌排序)
样本小而且有序的时候效率高
public void InsertionSort(int[] num){
int tmp=0;
for (int i=0;i<num.length-1;i++){
for (int j=i+1;j>0;j--){
if (num[j]<num[j-1]){
tmp = num[j];
num[j]= num[j-1];
num[j-1] = tmp;
}else {
break;
}
}
}
}
希尔排序(Shell Sort)
希尔排序也是一种插入排序
数据序列1: 13-17-20-42-28 利用插入排序,13-17-20-28-42. Number of swap:1;
数据序列2: 13-17-20-42-14 利用插入排序,13-14-17-20-42. Number of swap:3;
如果数据序列基本有序,使用插入排序会更加高效。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
public void ShellSort(int[] num){
int n = num.length,g=num.length,tmp=0;
while (g!=1){
g = g/2;
for (int i=0;i<g;i++){
for (int j=i+g;j<n;j+=g){
for (int k=j;k>i;k-=g){
if (num[k]<num[k-g]){
tmp = num[k];
num[k] = num[k-g];
num[k-g] = tmp;
}else {
break;
}
}
}
}
}
}
快速排序(Quicksort)
基本思想:(分治)
先从数列中取出一个数作为key值;
将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
对左右两个小数列重复第二步,直至各区间只有1个数。
快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用,再加上快速排序思想----分治法也确实实用,因此很多软件公司的笔试面试,包括像腾讯,微软等知名IT公司都喜欢考这个,还有大大小的程序方面的考试如软考,考研中也常常出现快速排序的身影。
下图是单轴快排
//快速排序(Quicksort)
public void QuickSort(int[] num,int low,int high){
if (low>=high){
return;
}
int i=low,j=high,k=num[low];
while (i<j){
while (i<j&&num[j]>=k){
j--;
}
if (i<j){
num[i] = num[j];
i++;
}
while (num[i]<k){
i++;
}
if (i<j){
num[j] = num[i];
j--;
}
}
num[i] = k;
QuickSort(num,low,i-1);
QuickSort(num,i+1,high);
}
归并排序(Merge Sort)
java对象排序一般要求稳定用的,以前是归并排序,现在应该是TimSort
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。
首先考虑下如何将2个有序数列合并。这个非常简单,只要从比较2个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。从上文的图中可看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。
public void MergeSort(int[] num){
int[] tmp = new int[num.length];
MergeSort(num,0,num.length,tmp);
}
private void MergeSort(int[] num ,int l,int r,int[] tmp){
if (l<r){
int mid = (r-l)/2+l;
MergeSort(num,l,mid,tmp);
MergeSort(num,mid+1,r,tmp);
merge(num,l,r,mid,tmp);
}
}
private static void merge(int[] num,int left,int mid,int right,int[] temp){
int i=left,j=mid+1,k=0;
while (i<mid&&j<right){
if (num[i]<num[j]){
temp[k]=num[i];
k++;
i++;
}else {
temp[k]=num[j];
k++;
j++;
}
}
while (i<mid){
temp[k++]=num[i++];
}
while (j<right){
temp[k++]=num[j++];
}
//将temp中的元素全部拷贝到原数组中
while(left <= right){
num[left++] = temp[k++];
}
}
堆排序(HeapSort)
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
a.假设给定无序序列结构如下:
此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。
找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
此时,我们就将一个无需序列构造成了一个大顶堆。
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
a.将堆顶元素9和末尾元素4进行交换
b.重新调整结构,使其继续满足堆定义
c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
再简单总结下堆排序的基本思路:
a.将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆(一般升序采用大顶堆,降序采用小顶堆);
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。
//堆排序(HeapSort)
public void HeapSort(int[] num){
//首先要把无序队列转化成大顶堆
for (int i=num.length/2 -1;i>0;i--){
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(num,i,num.length);
}
//2.调整堆结构+交换堆顶元素与末尾元素
for (int j=num.length-1;j>0;j--){
swap(num,0,j);//将堆顶元素与末尾元素进行交换
adjustHeap(num,0,j);//重新对堆进行调整
}
}
private void adjustHeap(int[] num,int i, int length){
int tmp = num[i];
for (int k=2*i+1;k<length;k=k*2+1){
if (k+1<length && num[k]<num[k+1]){
k++;
}
if (num[k]>tmp){
num[i]=num[k];
i=k;
}else {
break;
}
num[i] = tmp;
}
}
//交换
public static void swap(int []arr,int a ,int b){
int temp=arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
基数排序(RadixSort)
基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。
具体做法是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
/*
* 获取数组a中最大值
*
* 参数说明:
* a -- 数组
* n -- 数组长度
*/
private static int getMax(int[] a) {
int max;
max = a[0];
for (int i = 1; i < a.length; i++)
if (a[i] > max)
max = a[i];
return max;
}
/*
* 对数组按照"某个位数"进行排序(桶排序)
*
* 参数说明:
* a -- 数组
* exp -- 指数。对数组a按照该指数进行排序。
*
* 例如,对于数组a={50, 3, 542, 745, 2014, 154, 63, 616};
* (01) 当exp=1表示按照"个位"对数组a进行排序
* (02) 当exp=10表示按照"十位"对数组a进行排序
* (03) 当exp=100表示按照"百位"对数组a进行排序
* ...
*/
private static void countSort(int[] a, int exp) {
//int output[a.length]; // 存储"被排序数据"的临时数组
int[] output = new int[a.length]; // 存储"被排序数据"的临时数组
int[] buckets = new int[10];
// 将数据出现的次数存储在buckets[]中
for (int i = 0; i < a.length; i++)
buckets[ (a[i]/exp)%10 ]++;
// 更改buckets[i]。目的是让更改后的buckets[i]的值,是该数据在output[]中的位置。
for (int i = 1; i < 10; i++)
buckets[i] += buckets[i - 1];
// 将数据存储到临时数组output[]中
for (int i = a.length - 1; i >= 0; i--) {
output[buckets[ (a[i]/exp)%10 ] - 1] = a[i];
buckets[ (a[i]/exp)%10 ]--;
}
// 将排序好的数据赋值给a[]
for (int i = 0; i < a.length; i++)
a[i] = output[i];
output = null;
buckets = null;
}
/*
* 基数排序
*
* 参数说明:
* a -- 数组
*/
public static void radixSort(int[] a) {
int exp; // 指数。当对数组按各位进行排序时,exp=1;按十位进行排序时,exp=10;...
int max = getMax(a); // 数组a中的最大值
// 从个位开始,对数组a按"指数"进行排序
for (exp = 1; max/exp > 0; exp *= 10)
countSort(a, exp);
}
public static void main(String[] args) {
int i;
int a[] = {53, 3, 542, 748, 14, 214, 154, 63, 616};
System.out.printf("before sort:");
for (i=0; i<a.length; i++)
System.out.printf("%d ", a[i]);
System.out.printf("\n");
radixSort(a); // 基数排序
System.out.printf("after sort:");
for (i=0; i<a.length; i++)
System.out.printf("%d ", a[i]);
System.out.printf("\n");
}