此文章是女少侠本人在学习算法与数据结构过程中各类排序算法的笔记,十分感谢相关优秀博主的代码分享和经验总结,后面会附上参考链接,如有侵权,请联系删除。
各类排序算法总结
废话不多数,直接上图:
图片名词解释:
- n:数据规模
- k:桶的个数
- in-place:占用常数内存,不占用额外内存
- out-place:占用额外内存
算法分类
比较排序和非比较排序的区别
- 比较排序:快速排序、归并排序、堆排序、冒泡排序
每个数都必须和其它数比较,才能确定自己的位置
(1)冒泡排序类:问题规模为n,又因为需要比较k次,所以平均时间复杂度为O(n^2);
(2)归并排序、快速排序类:问题规模通过分治法消减为logN次,因此平均时间复杂度为O(nlogn)。 - 非比较排序:计数排序、基数排序、桶排序
针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置,因此非比较排序只需要确定每个元素之前的已有的元素个数即可,算法时间复杂度O(n);但由于非比较排序需要占用空间来确定位置,所以对数据规模和数据分布有一定的要求。
冒泡排序
算法步骤:
- 比较相邻的元素,如果第一个比第二个大,就交换它们两个
- 对每一对相邻元素做相同工作,最后最大的数就会到数组的末尾
- 针对所有的元素重复以上操作,除了已经排到最后的大数
- 重复1~3,直到排序完成
/*
*冒泡排序
*/
public int[] bubbleSort(int[] arr){
if(arr.length==0)return arr;
for(int i = 0;i < arr.length;i++){
for(int j = 0;j < arr.length-1-i;j++){
if(arr[j+1]<arr[j]){ //将较大者放到后面
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
return arr;
}
算法分析:
最佳情况:T(n) = O(n) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2)
选择排序
算法步骤:
- 初始状态:无序区为R[1,2,…n],有序区为空
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区
- n-1趟结束,数组有序化
/*
*选择排序
*/
public int[] selectionSort(int[] arr){
if(arr.length==0)return arr;
for(int i = 0;i < arr.length;i++){
int minIndex = i;
for(int j = i+1;j < arr.length;j++){
if(arr[j]<arr[minIndex])minIndex = j;//找到最小的数的下标
}
int tmp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = tmp;
}
return arr;
}
算法分析:
最佳情况:T(n) = O(n2) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2)
插入排序
算法步骤:
- 第一个元素默认已排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素大于新元素,将该元素移到下一位置
- 重复步骤三,直到找到已排序的元素或者等于新元素的位置
- 将新元素插入到该位置
- 重复步骤2~5
/*
*插入排序
*/
public int[] insertionSort(int[] arr){
if(arr.length==0)return arr;
int current;
for(int i = 0;i < arr.length-1;i++){
int preIndex = i;
current = arr[i+1];
//将元素不断往前移,直到找到第一个比它小的元素,并放在后面
while(preIndex>=0 && current<arr[preIndex]){
arr[preIndex+1] = arr[preIndex];
preIndex--;
}
arr[preIndex+1] = current;
}
return arr;
}
算法分析:
最佳情况:T(n) = O(n) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2)
希尔排序——插入排序的改版
算法步骤:
- 选择一个增量序列t1, t2,…, tk,其中ti>tj,tk=1
- 按增量序列个数为k,对序列进行k趟排序
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m的子序列,分别对各子表进行直接插入排序。仅增量为1时,整个序列作为一个表来处理,表长度即为整个序列的长度
/*
*希尔排序
*/
public int[] shellSort(int[] arr){
int len = arr.length;
int tmp,gap = len/2;
while(gap > 0){
for(int i = gap;i < len;i++){
tmp = arr[i];
int preIndex = i-gap;
while(preIndex>=0 && tmp<arr[preIndex]){
arr[preIndex+gap] = arr[preIndex];
preIndex-=gap;
}
arr[preIndex+gap] = tmp;
}
gap/=2;
}
return arr;
}
算法分析:
最佳情况:T(n) = O(nlog2n) 最坏情况:T(n) = O(nlog2n) 平均情况:T(n) =O(nlog2n)
归并排序
算法步骤:
- 把长度为n的输入序列分成两个长度为n/2的子序列
- 对这两个子序列分别采用归并排序
- 将两个排序好的子序列合并成一个最终的排序序列
/*
*归并排序
*/
public int[] mergeSort(int[] arr){
int mid = arr.length/2;
//对数组左半部分进行归并排序
int[] left = Arrays.copyOfRange(arr,0,mid);
//对数组右半部分进行归并排序
int[] right = Arrays.copyOfRange(arr,mid+1,arr.length);
//合并左右部分数组
return Merge(mergeSort(left),mergeSort(right));
}
//归并排序——将两个有序数组合并成一个有序数组
public int[] Merge(int[] left, int[] right){
int[] ans = new int[left.length + right.length];
for(int index=0,i=0,j=0;index < arr.length;index++){
if(i>=left.length)arr[index] = right[j++];
else if(j>=right.length)arr[index] = left[i++];
else if(left[i]<right[j])arr[index] = left[i];
else arr[index] = right[j];
}
return arr;
}
算法分析:
最佳情况:T(n) = O(n) 最差情况:T(n) = O(nlogn) 平均情况:T(n) = O(nlogn)
快速排序
算法步骤:
- 从数列中挑出一个元素,称为“基准”
- 重新排序数列,所有元素比基准值小的摆放在基准前面,比基准值大的放在后面(相同的数可以放任意一边),在这个分区退出后,该基准就处于数列的中间位置。这个称为分区操作
- 递归地把小于基准值元素的子数列和大于基准值元素的子序列排序
/*
*快速排序
*/
public int[] quickSort(int[] arr,int low,int high){
int i,j,tmp;
if(arr.length<2)return arr;
if(high<low||high>=arr.length||low<0)return null;
i = low;
j = high;
tmp = arr[low];
while(i<j){
while(i<j && arr[j]>tmp)j--;
while(i<j && arr[i]<tmp)i++;
if(i<j)swap(arr,i,j);
}
//此时左右指针来到了同一位置
arr[low] = arr[i];
arr[i] = tmp;
//递归调用左右半边数组
quickSort(arr,0,i-1);
quickSort(arr,j+1,high);
return arr;
}
//交换数组的两个位置上的元素
public void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
算法分析:
最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(nlogn)
堆排序
算法步骤:
- 将初始待排序关键字序列(R1,R2,…,Rn),此堆为初始的无序堆
- 将堆顶元素R1与最后一个元素Rn交换,此时得到的新的无序区(R1,R2,…,Rn-1)和新的有序区(Rn),且满足R[1,2,…n-1]<=R[n]
- 由于交换后的新的堆顶R1可能违反堆的性质,因此需要对当前无序区(R1,R2,…,Rn-1)调整为新堆,然后再次将R1与无序区最后一个元素交换,得到新的无序区(R1,R2,…,Rn-2)和新的有序区(Rn-1,Rn)
- 不断重复上述过程直到有序区的元素个数为n-1,则整个排序过程完成
/*
*堆排序
*/
static int len;
public int[] heapSort(int[] arr){
len = arr.length;
//构建最大堆
buildMaxHeap(arr);
//将第一个节点与末尾节点进行交换
while(len>0){
swap(arr,0,len-1);
len--;
adjustHeap(arr,0);
}
return arr;
}
//构建最大堆
public void buildMaxHeap(int[] arr){
//从第一个不是叶子节点的节点开始
for(int i = len/2-1;i>=0;i--){
adjustHeap(arr,i);
}
}
//调整使之成为最大堆
public void adjustHeap(int[] arr,int i){
int maxIndex = i;
//左节点比父节点大
if(i*2<len && arr[i*2]>arr[maxIndex])maxIndex = i*2;
//右节点比父节点大
if(i*2+1<len && arr[i*2+1]>arr[maxIndex])maxIndex = i*2+1;
//父节点的值不是最大节点,交换并调整最大堆
if(i!=maxIndex){
swap(arr,i,maxIndex);
adjustHeap(arr,maxIndex);
}
}
算法分析:
最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(nlogn) 平均情况:T(n) = O(nlogn)
计数排序——适用于小范围数组
算法步骤:
- 找出待排序数组中最大和最小的元素
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
- 对所有的计数累加(从C的第一个元素开始,每一项和前一项相加)
- 反向填充数组:将每个元素i放在新数组的C(i)项,没放一个元素就将C(i)减去1
/*
*计数排序
*/
public int[] countingSort(int[] arr){
if(arr.length<2)return arr;
//找出最大值和最小值
int maxNum = arr[0];
int minNum = arr[0];
for(int i = 1;i < arr.length;i++){
if(arr[i]>maxNum)maxNum = arr[i];
if(arr[i]<minNum)minNum = arr[i];
}
int[] bucket = new int[maxNum-minNum+1];
int bias = 0-minNum;
//计数
for(int i = 0;i < arr.length;i++){
bucket[arr[i]+bias]++;
}
//取出桶中的数
int index = 0,i = 0;
while(index<arr.length){
if(bucket[i]!=0){
arr[index] = i-bias;
index++;
bucket[i]--;
}else{
i++;
}
}
return arr;
}
算法分析:
最佳情况:T(n) = O(n+k) 最差情况:T(n) = O(n+k) 平均情况:T(n) = O(n+k)
桶排序
算法步骤:
- 人为设置一个BucketSize,作为每个桶所能放置多少个不同的值
- 遍历输入数据,把数据一个个放到对应的桶里去
- 对每个不是空的桶进行排序,可以使用其它排序算法,也可以递归使用桶排序
- 从不是空的桶里把排好序的数据拼接起来
注意:
如果递归使用桶排序为各个桶排序,则当桶数量为1时要手动减少BucketSize增加下一循环桶的数量,否则会陷入死循环
/**
* 桶排序
* @param array
* @param bucketSize
* @return
*/
public static ArrayList<Integer> BucketSort(ArrayList<Integer> array, int bucketSize) {
if (array == null || array.size() < 2)
return array;
int max = array.get(0), min = array.get(0);
// 找到最大值最小值
for (int i = 0; i < array.size(); i++) {
if (array.get(i) > max)
max = array.get(i);
if (array.get(i) < min)
min = array.get(i);
}
int bucketCount = (max - min) / bucketSize + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
ArrayList<Integer> resultArr = new ArrayList<>();
for (int i = 0; i < bucketCount; i++) {
bucketArr.add(new ArrayList<Integer>());
}
for (int i = 0; i < array.size(); i++) {
bucketArr.get((array.get(i) - min) / bucketSize).add(array.get(i));
}
for (int i = 0; i < bucketCount; i++) {
if (bucketSize == 1) { // 如果带排序数组中有重复数字时 感谢 @见风任然是风 朋友指出错误
for (int j = 0; j < bucketArr.get(i).size(); j++)
resultArr.add(bucketArr.get(i).get(j));
} else {
if (bucketCount == 1)
bucketSize--;
ArrayList<Integer> temp = BucketSort(bucketArr.get(i), bucketSize);
for (int j = 0; j < temp.size(); j++)
resultArr.add(temp.get(j));
}
}
return resultArr;
}
算法分析:
最佳情况:T(n) = O(n+k) 最差情况:T(n) = O(n+k) 平均情况:T(n) = O(n2)
基数排序
算法步骤:
- 取数组中的最大数,并取得位数
- arr为原始数组,从最低位开始取每个位组成Radix数组
- 对Radix进行计数排序(利用计数排序适用于小范围数的特点)
/*
*基数排序
*/
public int[] radixSort(int[] arr){
if(arr.length<2)return arr;
int maxNum = arr[0];
int maxDigit = 0;//记录最大数的位数
//找出数组中最大值
for(int i = 0;i < arr.length;i++){
maxNum = Math.max(arr[i],maxNum);
}
while(maxNum!=0){
maxNum/=10;
maxDigit++;
}
//初始化桶
List<List<Integer>> bucket = new ArrayList<>();
for(int i = 0;i < 10;i++){
bucket.add(new ArrayList<Integer>());
}
int mod = 10;
int div = 1;
for(int i = 0;i < maxDigit;i++,mod*=10,div*=10){
//依据位值放入相应桶中
for(int j = 0;j < arr.length;j++){
int num = (arr[j]%mod)/div;
bucket.get(num).add(arr[j]);
}
int index = 0;
//取出桶中的数
for(int j = 0;j < bucket.size();j++){
for(int k = 0;k < bucket.get(j).size();k++){
arr[index++] = bucket.get(j).get(k);
}
bucket.get(j).clear();
}
}
return arr;
}
算法分析:
最佳情况:T(n) = O(n * k) 最差情况:T(n) = O(n * k) 平均情况:T(n) = O(n * k)