赞
踩
int i = 1;
i++;
无论代码执行了多少行,只要没有循环等复杂的结构,时间复杂度都是O(1)
while(i<n) {
i = i*2;
}
此处i并不是依次递增到n,而是每次都以倍数增长。假设循环了x次后i大于n。则 2 x = n 2^x = n 2x=n, x = l o g 2 n x=log_2n x=log2n
for(int i = 0; i<n; i++) {
i++;
}
这其中,循环体中的代码会执行n+1次,时间复杂度为 O ( n ) O(n) O(n)
for(int i = 0; i<n; i++) {
j = 1;
while(j<n) {
j = j*2;
}
}
此处外部为一个循环,循环了n次。内部也是一个循环,但内部f循环的时间复杂度是
l
o
g
2
n
log_2n
log2n
所以总体的时间复杂度为线性对数阶
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)
for(int i = 0; i<n; i++) {
for(int j = 0; j<n; j++) {
//循环体
}
}
for(int i = 0; i<n; i++) {
for(int j = 0; j<n; j++) {
for(int k = 0; k<n; k++) {
//循环体
}
}
}
可以看出平方阶、立方阶的复杂度主要是否循环嵌套了几层来决定的.
常见的算法时间复杂度由小到大依次为: 0 ( 1 ) < O ( l o g 2 n ) < O ( n ) < O ( n l o g 2 n ) < 0 ( n 2 ) < o ( n 3 ) < O ( n k ) < O ( 2 n ) 0(1)<O(log_2n)<O(n)<O(nlog_2n)<0(n^2)<o(n^3)< O(n^k) < O(2^n) 0(1)<O(log2n)<O(n)<O(nlog2n)<0(n2)<o(n3)<O(nk)<O(2n) ,随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
把n
个待排序的元素看成为一个有序表
和一个无序表
,开始时有序表
中只包含1
个元素,无序表
中包含有n-1
个元素,排序过程中每次从无序表中取出第一个
元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
package pers.chh3213.sort; import java.util.Arrays; /** * DirectInsertSort.java * @Description 直接插入法 * @author chh3213 * @version * @date 2021年12月26日上午11:19:07 */ public class DirectInsertSort { public static void main(String[] args) { DirectInsertSort insertSort = new DirectInsertSort(); int[] arr = {9, -16, 310, 23, -30, -49, 25, 21, 30}; insertSort.directInsertSort(arr); System.out.println(Arrays.toString(arr)); } public void directInsertSort(int[] arr) { // 法一 for (int i = 1; i < arr.length; i++) { int temp = arr[i]; // 记录要插入的数据 int j=i; for ( ; j>0&&arr[j-1]>temp; j--) {// 从已经排序的序列最右边的开始比较,找到比其小的数 arr[j]=arr[j-1]; //元素后移 } if(j!=i)arr[j]=temp; } //法二 // for (int i = 1; i < arr.length; i++) { // for(int j=i;j>0;j--) { // if(arr[j]<arr[j-1])swap(arr, j, j-1); // } // } } public void swap(int[] arr, int i, int j) { int temp =arr[i]; arr[i]=arr[j]; arr[j]=temp; } }
C++实现
#include <iostream> #include <vector> using namespace std; void directSort(vector<int>&arr){ for(int i=0;i<arr.size();i++){ int tmp=arr[i]; int j=i; for(;j>0&&arr[j-1]>tmp;j--){ arr[j]=arr[j-1]; } if(j!=i)arr[j]=tmp; } } int main(){ vector<int>arr={9, -16, 310, 23, -30, -49, 25, 21, 30}; directSort(arr); for(int a:arr){ cout<<a<<' '; } return 0; }
n-1
次插入,每次插入最少比较一次(正序),移动0
次;最多比较i
次(包括同temp的比较),移动i+1
次(逆序)(i=2,3,…,n)。因此,直接插入排序的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2) 。稳定
的。先将整个待排元素序列分割成若干个子序列(由相隔某个增量
的元素组成的)分别进行直接插入排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况) ,效率是很高的,因此希尔排序在时间效率上有较大提高。
d1=8/2=4;
d2=4/2=2;
d3=2/2=1;
选择一个增量序列 t1,t2,……,tk
,其中 ti > tj
, tk = 1
;
按增量序列个数 k
,对序列进行 k
趟直接插入排序;
每趟排序,根据对应的增量 ti
,将待排序列分割成若干长度为 m
的子序列,分别对各子表进行直接插入排序。仅增量因子为 1
时,整个序列作为一个表来处理,表长度即为整个序列的长度。
java实现
package pers.chh3213.sort; import java.util.Arrays; public class ShellSort { public static void main(String[] args) { ShellSort shell = new ShellSort(); int[] arr = {9, -16, 310, 23, -30, -49, 25, 21, 30}; shell.shellSort(arr); System.out.println(Arrays.toString(arr)); } public void shellSort(int[] arr) { //第一次步长为数组长度/2,后面依次步长/2 for (int step = arr.length /2; step >0; step /= 2) { //直接插入排序 for (int i = step; i < arr.length; i++) { int temp = arr[i];//右侧待排序区第一个数 int j=i-step;//左侧已排序区域第一个数索引 for( ;j>=0&&arr[j]>temp;j-=step) { arr[j+step]=arr[j];//满足条件则把已排序数字往后挪一个位置 // swap(arr, j, j+step); } //插入待排序数字到指定位置 arr[j+step]=temp; } } } public void swap(int[] arr, int i, int j) { int temp =arr[i]; arr[i]=arr[j]; arr[j]=temp; } }
C++实现
#include <iostream>
#include <vector>
using namespace std;
void directSort(vector<int>&arr){
for(int i=0;i<arr.size();i++){
int tmp=arr[i];
int j=i;
for(;j>0&&arr[j-1]>tmp;j--){
arr[j]=arr[j-1];
}
if(j!=i)arr[j]=tmp;
}
}
n
数量级的,内循环远远低于n
数量级,因为当分组较多时,组内元素少此循环次数少;当分组较少时,组内元素增多,但已接近有序,循环次数并不增加。因此,希尔排序的时间复杂性在
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)和
O
(
n
2
)
O(n^2)
O(n2)之间,大致为
O
(
n
1.3
)
O(n^{1.3})
O(n1.3) 。不稳定
的。冒泡排序通过重复地走访要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。
原理描述
通过对待排序序列从前向后,依次比较相邻元素的排序码,若发现逆序则交换,使排序码较大的元素逐渐从前部移向后部。
实现步骤(默认升序的情况)
package pers.chh3213.sort; import java.util.Iterator; import java.util.Scanner; public class BubbleSort { public static void main(String[] args) { int[] arr = {5,4,1,966,2,3,56,89,12,0,56562}; System.out.println("before sort:"); for (int i : arr) { System.out.print(i+"\t"); } for (int i = 0; i < arr.length-1; i++) { for (int j = 0; j < arr.length-1-i; j++) { if(arr[j]>arr[j+1]) { int temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; } } } System.out.println(); System.out.println("after sort:"); for (int i : arr) { System.out.print(i+"\t"); } } }
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序, 因此可以在排序过程中设置一个标志swap判断元素是否进行过交换,从而减少不必要的比较。改进代码如下:
public void bubbleSort(int[] data) {
for (int i = 0; i < data.length-1; i++) {
boolean swap = false;
//每次排序会确定一个最大的元素
for (int j = 0; j < data.length-i-1; j++) {
if(data[j]>data[j+1]) {
int temp = data[j];
data[j]= data[j+1];
data[j+1] = temp;
swap = true;
}
}
if(!swap)break; //如果第一趟并未发生交换,说明原数组有序,故停止排序
}
}
C++实现
void bubbleSort(vector<int>&arr){
for(int i=0;i<arr.size()-1;i++){
bool flag=false;
for(int j=0;j<arr.size()-i-1;j++){
if(arr[j]>arr[j+1]){{
int tmp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=tmp;
flag=true;
}}
}
if(!flag)break;
}
}
(n-1)
次,移动元素次数为0
;若待排序的元素为逆序,则需进行n-1
趟排序,比较次数为
(
n
2
−
n
)
/
2
(n^2-n)/2
(n2−n)/2,移动次数为
3
(
n
2
−
n
)
/
2
3(n^2-n )/2
3(n2−n)/2,因此冒泡排序算法的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2) 。由于其中的元素移动较多,所以属于内排序中速度较慢的一种。因为冒泡排序算法只进行元素间的顺序移动,所以是一个稳定的算法。一次划分的具体过程
概括地说,一次划分就是从表的两端交替地向中间进行扫描,将小的放到左边,大的放到右边,作为标准的元素放到中间。
之后采用递归的方式分别对前半部分和后半部分排序,当前半部分和后半部分均有序时该数组就自然有序了。
一次划分的具体过程示例
8. 直到low==high时, R[low]=base (即将作为标准的元素放到其最终位置)。
- 示例
package pers.chh3213.sort; public class QuickSort { public static void main(String[] args) { System.out.println("quick sort test"); int[] arr = {9, -16, 30, 23, -30, -49, 25, 21, 30}; System.out.println("before sort:"); for (int i : arr) { System.out.print(i+"\t"); } QuickSort quick = new QuickSort(); quick.quickSort(arr, 0, arr.length-1); System.out.println(); System.out.println("after sort:"); for (int i : arr) { System.out.print(i+"\t"); } } public void quickSort(int[] arr,int start, int end) { if(start<end) { int index = partition(arr, start, end); //将表一分为2 quickSort(arr, start, index-1); // 对左子序列进行快速排序 quickSort(arr, index+1, end); //对右子序列进行快速排序 } } // 一次划分 public int partition(int[] arr, int low,int high) { int base = arr[low]; //暂存基准元素到base while (low<high) {//从表的两端交替的向中间扫描 while(low<high && arr[high]>=base)high--;//右端扫描 if(low<high) { arr[low]=arr[high];//把比基准小的元素放到基准前面 low++; } while(low<high && arr[low]< base)low++;//左端扫描 if(low<high) { arr[high]=arr[low];//把比基准大的元素放到基准后面 high--; } } arr[low] = base;//把基准元素放到最终位置 return low;//返回基准元素所在的位置 } }
c++实现
int partition(vector<int>&arr,int low,int high){ int base = arr[low]; //暂存基准元素到base while (low<high) {//从表的两端交替的向中间扫描 while(low<high && arr[high]>=base)high--;//右端扫描 if(low<high) { arr[low]=arr[high];//把比基准小的元素放到基准前面 low++; } while(low<high && arr[low]< base)low++;//左端扫描 if(low<high) { arr[high]=arr[low];//把比基准大的元素放到基准后面 high--; } } arr[low] = base;//把基准元素放到最终位置 return low;//返回基准元素所在的位置 } void quickSort(vector<int>&arr,int start,int end){ if(start<end) { int index = partition(arr, start, end); //将表一分为2 quickSort(arr, start, index-1); // 对左子序列进行快速排序 quickSort(arr, index+1, end); //对右子序列进行快速排序 }
快速排序的递归过程可用一棵二叉树形象地给出。下图为待排序列49,38,65,97,76,13,27,49
所对应的快速排序递归调用过程的二叉树(简称为快速排序递归树)。
从快速排序算法的递归树可知,快速排序的趟数取决于递归树的高度。
如果每次划分对一个对象定位后,该对象的左子序列与右子序列的长度相同,则下一步将是对两个长度减半的子序列进行排序,这是最理想的情况。
假设n是2的幂,
n
=
2
k
,
(
k
=
l
o
g
2
n
)
n=2^k,(k=log_2n)
n=2k,(k=log2n),假设基准位置位于序列中间,这样划分的子区间大小基本相等。
n
+
2
(
n
/
2
)
+
4
(
n
/
4
)
+
.
.
.
+
n
(
n
/
n
)
=
n
+
n
+
.
.
.
+
n
=
n
∗
k
=
n
∗
l
o
g
2
n
n+2(n/2)+4(n/4)+ ...+n(n/n)=n+n+ ...+n=n*k=n*log_2n
n+2(n/2)+4(n/4)+...+n(n/n)=n+n+...+n=n∗k=n∗log2n
因此,快速排序的最好时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) 。而且在理论上已经证明,快速排序的平均时间复杂度也为 O ( n l o g 2 n ) O (nlog_2n) O(nlog2n) 。实验结果表明:就平均计算时间而言,快速排序是所有内排序方法中最好的一个。
在最坏的情况,即待排序对象序列已经按其排序码从小到大排好序的情况下,其递归树成为单支树,每次划分只得到一个比上一次少一个对象的子序列(蜕化为冒泡排序)。必须经过n-1
趟才能把所有对象定位,而且第i
趟需要经过n-i
次排序码比较才能找到第i
个对象的安放位置,总的排序码比较次数将达到
∑
i
=
1
n
−
1
(
n
−
i
)
=
1
2
n
(
n
−
1
)
≈
n
2
2
\sum_{i=1}^{n-1}{(n-i)}=\frac{1}{2}n(n-1) \approx \frac{n^2}{2}
∑i=1n−1(n−i)=21n(n−1)≈2n2
因此,快速排序的最坏时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。
栈
存放每层递归调用时的指针和参数。最大递归调用层次数与递归树的高度一致。理想情况为
l
o
g
2
(
n
+
1
)
log_2(n+1)
log2(n+1);最坏情况即待排序对象序列已经按其排序码从小到大排好序的情况下,其递归树成为单支树,深度为n
。因此,快速排序最好的空间复杂度为
O
(
l
o
g
2
n
)
O (log_2n)
O(log2n) ,最坏的空间复杂度为
O
(
n
)
O(n)
O(n)(即快速排序所需用的辅助空间)。R[i]
到R[n]
中选择具有最小关键码的元素package pers.chh3213.sort; import java.util.Arrays; public class SelectSort { public static void main(String[] args) { SelectSort select = new SelectSort(); int[] arr = {9, -16, 310, 23, -30, -49, 25, 21, 30}; select.selectSort(arr); System.out.println(Arrays.toString(arr)); } public void selectSort(int[] arr) { for (int i = 0; i < arr.length-1; i++) {//进行n-1趟排序 int min = i; for (int j = i+1; j < arr.length; j++) { if(arr[min]>arr[j])min=j; // 记录目前能找到的最小值元素的下标 } // 找到最小值后,再将找到的最小值和i位置所在的值进行交换 if(i!=min)swap(arr, i, min); } } public void swap(int[] arr, int i, int j) { int temp =arr[i]; arr[i]=arr[j]; arr[j]=temp; } }
C++实现
void selectSort(vector<int>&arr){
for (int i = 0; i < arr.size()-1; i++) {//进行n-1趟排序
int min = i;
for (int j = i+1; j < arr.size(); j++) {
if(arr[min]>arr[j])min=j; // 记录目前能找到的最小值元素的下标
}
// 找到最小值后,再将找到的最小值和i位置所在的值进行交换
if(i!=min) {
int temp =arr[i];
arr[i]=arr[min];
arr[min]=temp;
};
}
}
无论初始状态如何,在第i
趟排序中选择最小关键码的元素,需做n-i
次比较,因此总的比较次数为:
∑
i
=
1
n
−
1
n
−
i
=
n
(
n
−
1
)
/
2
=
O
(
n
2
)
\sum_{i=1}^{n-1}{n-i}=n(n-1)/2=O(n^2)
∑i=1n−1n−i=n(n−1)/2=O(n2)(即时间复杂度)
最好情况:序列为正序时,移动次数为 0 0 0, 最坏情况:序列为反序时,每趟排序均要执行交换操作,总的移动次数取最大值 3 ( n − 1 ) 3(n-1) 3(n−1)。
由于在直接选择排序中存在着不相邻元素之间的互换,因此,直接选择排序是一种不稳定的排序方法。例如,给定排序码为3, 7, 3’, 2, 1,排序后的结果为1, 2, 3’, 3, 7
n个元素的序列{k1, k2 , .... , kn }
,当且仅当满足
{
k
i
≤
k
2
i
k
i
≤
k
2
i
+
1
(1)
或者
{
k
i
≥
k
2
i
k
i
≥
k
2
i
+
1
(1)
称之为堆。
若将此排序码按顺序组成一棵完全二叉树,则(1)称为小顶堆(二叉树的所有结点值小于或等于左右孩子的值) , (2)称为**大顶堆(**二叉树的所有结点值大于或等于左右孩子的值).
一般升序排序采用大顶堆,降序排序使用小顶堆
小顶堆和大顶堆示例
建初始堆
将排序码k1, k2, k, ..,kn
表示成一棵完全二叉树,然后从第n/2
个排序码(即树的最后一个非终端结点)开始筛选,使由该结点作根结点组成的子二叉树符合堆的定义
,然后从第n/2-1
个排序码重复刚才操作,直到第一个排序码止。这时候,该二叉树符合堆的定义,初始堆已经建立。
堆排序
将堆中第一个结点(二叉树根结点)和最后一个结点的数据进行交换(k1,与kn,),再将
k
1
k_1
k1~
k
n
−
1
k_{n-1}
kn−1,重新建堆,然后
k
1
k_1
k1和
k
n
−
2
k_{n-2}
kn−2交换,如此重复下去,每次重新建堆的元素个数不断减1,直到重新建堆的元素个数仅剩一个为止。这时堆排序已经完成,则排序码
k
1
,
k
2
,
k
3
,
.
.
,
k
n
k1, k2, k3, .., kn
k1,k2,k3,..,kn已排成一个有序序列。
堆排序的两大步骤
堆排序的关键问题
package pers.chh3213.sort; import java.util.Arrays; public class HeapSort { public static void main(String[] args) { HeapSort hSort = new HeapSort(); int[] arr = {9, 160, -31, 25, -30, 49, 25, 21, 30}; hSort.heapSort(arr); System.out.println(Arrays.toString(arr)); } public int[] heapSort(int[] arr) { buildMaxHeap(arr); int len = arr.length; for (int i = len - 1; i > 0; i--) { swap(arr, 0, i); len--; heapify(arr, 0, len); } return arr; } private void buildMaxHeap(int[] arr) { int len = arr.length; for (int i = (int) Math.floor(len / 2); i >= 0; i--) { heapify(arr, i, len); } } private void heapify(int[] arr, int i, int len) { int left = 2*i+1; int right = 2*i+2; int largest = i; if(left<len&&arr[left]>arr[largest])largest=left; if(right<len&&arr[right]>arr[largest])largest=right; if(largest!=i) { swap(arr, i, largest); heapify(arr, largest, len); } } /** * 交换元素 * @param arr * @param i * @param j */ private void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
示例
(图片来源:参考资料3)
特点
package pers.chh3213.sort; import java.util.Arrays; /** * * MergeSort.java * @Description 归并排序 * @author chh3213 * @version * @date 2021年12月26日下午4:54:53 */ public class MergeSort { public static void main(String[] args) { MergeSort Sort = new MergeSort(); int[] arr = {9, -16, 310, 23, -30, -49, 25, 21, 30}; Sort.mergeSort(arr,0,arr.length-1); System.out.println(Arrays.toString(arr)); } public void mergeSort(int[] arr,int left, int right) { int mid = (left+right)/2; if(left<right) { //递归 mergeSort(arr, left, mid);//左边归并排序,使得左子序列有序 mergeSort(arr, mid+1, right);//右边归并排序,使得右子序列有序 //合并 merge(arr, left, right, mid);//将两个有序子数组合并操作 } } public void merge(int[] arr, int left,int right, int mid) { /* * 将两个有序数组合并 */ int[] temp = new int[right-left+1]; //建好一个临时数组 int i=left; //左子序列索引(理解成指针) int j = mid+1;//右子序列索引(理解成指针) int k =0;//临时数组的索引(理解成指针) while(i<=mid && j<=right) { if(arr[i]<=arr[j]) { temp[k++]=arr[i++]; } else { temp[k++]=arr[j++]; } } while(i<=mid)temp[k++]=arr[i++];//将左子序列剩余元素填充进temp中 while(j<=right)temp[k++]=arr[j++];//将右子序列剩余元素填充进temp中 //将temp中的元素全部拷贝回原数组中 for (int k2 = 0; k2 < temp.length; k2++) { arr[k2+left]=temp[k2]; } } }
n
个元素的表,将这n
个元素看作叶结点,若将两两归并生成的子表看作它们的父结点,则归并过程对应由叶向根生成一棵二叉树的过程。所以归并趟数等于二叉数的高度减1,即
l
o
g
2
n
log_2n
log2n。每一趟归并需移动n
个元素,即每一趟归并的时间复杂度为
O
(
n
)
O(n)
O(n)。因此, 二路归并排序的时间复杂度为
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)。找出原数组中元素值最大的,记为max
。
创建一个新数组count
,其长度是max
加1,其元素默认值都为0。
遍历原数组中的元素,以原数组中的元素作为count
数组的索引,以原数组中的元素出现次数作为count
数组的元素值。
创建结果数组result
,起始索引index
。
遍历count
数组,找出其中元素值大于0的元素,将其对应的索引作为元素值填充到result
数组中去,每处理一次,count
中的该元素值减1,直到该元素值不大于0,依次处理count
中剩下的元素。
第六步:返回结果数组result
。
package pers.chh3213.sort; import java.util.Arrays; /** * * CountingSort.java * @Description 计数排序 * @author chh3213 * @version * @date 2022年2月15日上午10:46:13 */ public class CountingSort{ public static void main(String[] args) { CountingSort cSort = new CountingSort(); int[] arr = {9, 16, 31, 23, 30, 49, 25, 21, 30}; int[] result = cSort.countingSort(arr); System.out.println(Arrays.toString(result)); } public int[] countingSort(int[] arr) { int maxValue = getMaxValue(arr); // 创建一个新数组,长度为max+1 int countLen = maxValue+1; int[] count = new int[countLen]; //统计数组中每个值为i的元素出现的次数,存入数组的第i项 for (int i : arr) { count[i]++; } int[] result = new int[countLen]; // 创建结果数组的起始索引 int sortedIndex = 0; // 遍历计数数组,将计数数组的索引填充到结果数组中 for (int i = 0; i < countLen; i++) { while(count[i]>0) { result[sortedIndex++]=i; count[i]--; } } return result; } /** * 找出待排序的数组中最大的元素 * @param arr 待排序的数组 * @return 返回最大值 */ public int getMaxValue(int[] arr) { int maxValue = arr[0]; for (int i = 0; i < arr.length; i++) { if(maxValue<arr[i]) { maxValue=arr[i]; } } return maxValue; } }
基本思想能够解决一般的情况,但是存在空间浪费的问题。比如一组数据{101,109,108,102,110,107,103},其中最大值为110,按照基础版的思路,我们需要创建一个长度为111的计数数组,但是我们可以发现,它前面的[0,100]的空间完全浪费了,那怎样优化呢?
将数组长度定为max-min+1,即不仅要找出最大值,还要找出最小值,根据两者的差来确定计数数组的长度。
package pers.chh3213.sort; import java.util.Arrays; import java.util.Iterator; /** * * CountingSort2.java * @Description 计数排序 * @author chh3213 * @version * @date 2022年2月15日上午10:46:13 */ public class CountingSort2{ public static void main(String[] args) { CountingSort2 cSort = new CountingSort2(); int[] arr = {9, 16, -31, 23, 30, 49, 25, 21, 30}; int[] result = cSort.countingSort(arr); System.out.println(Arrays.toString(result)); } public int[] countingSort(int[] arr) { int maxValue = getMaxValue(arr); int minValue = getMinValue(arr); // 创建一个新数组,长度为max+1 int countLen = maxValue-minValue+1; // System.out.println(countLen); int[] count = new int[countLen]; //统计数组中每个值为i的元素出现的次数,存入数组的第i项 for (int i : arr) { // A中的元素要减去最小值,再作为新索引 count[i-minValue]++; } int[] result = new int[countLen]; // 创建结果数组的起始索引 int sortedIndex = 0; // 遍历计数数组,将计数数组的索引填充到结果数组中 for (int i = 0; i < countLen; i++) { while(count[i]>0) { // 再将减去的最小值补上 result[sortedIndex++]=i+minValue; count[i]--; } } return result; } /** * 找出待排序的数组中最大的元素 * @param arr 待排序的数组 * @return 返回最大值 */ public int getMaxValue(int[] arr) { int maxValue = arr[0]; for (int i = 0; i < arr.length; i++) { if(maxValue<arr[i]) { maxValue=arr[i]; } } return maxValue; } /** * 找出待排序的数组最小的元素 * @param arr 待排序的数组 * @return 返回最小值 */ public int getMinValue(int[] arr) { int minValue = arr[0]; for (int i = 0; i < arr.length; i++) { if(minValue>arr[i]) { minValue=arr[i]; } } return minValue; } }
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是$ O(n + k)$。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
通俗地理解,例如有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
什么时候最快
当输入的数据可以均匀的分配到每一个桶中。
什么时候最慢
当输入的数据被分配到了同一个桶中。
示意图
元素分布在桶中:
然后,元素在每个桶中排序:
得到无序数组的取值范围
根据取值范围"创建"对应数量的"空桶"。
遍历数组,把数据放到对应的桶中。
(注:图片来源:参考资料7)
对每个不为空的桶中数据进行排序。对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
拼接不为空的桶中数据,得到结果
"桶"是一种容器,这个容器可以用多种数据结构实现,包括数组、队列或者栈。
(注:图片来源:参考资料1)
package pers.chh3213.sort; import java.lang.reflect.Array; import java.util.Arrays; /** * * BucketSort.java * @Description 桶排序 * @author chh3213 * @version * @date 2022年2月15日下午2:20:23 */ public class BucketSort { //使用快速排序对每个桶进行排序 QuickSort quickSort = new QuickSort(); public static void main(String[] args) { BucketSort bucket = new BucketSort(); int[] arr = {9, 160, -31, 23, -30, 49, 25, 21, 30}; arr = bucket.bucketSort(arr, 5); System.out.println(Arrays.toString(arr)); } /** * 桶排序 * @param arr 待排序数组 * @param bucketSize 桶的大小(容量) * @return */ public int[] bucketSort(int[] arr, int bucketSize) { if(arr.length==0)return arr; //获取最大、最小值 int maxValue = arr[0]; int minValue = arr[0]; for (int i : arr) { if(i<minValue)minValue=i; if(i>maxValue)maxValue=i; } //桶的数量 int bucketCount = (int)Math.floor((maxValue-minValue)/bucketSize)+1; //根据取值范围"创建"对应数量的"桶" int[][] buckets = new int[bucketCount][0]; // 利用映射函数将数据分配到各个桶中 for (int i = 0; i < arr.length; i++) { int index = (int)Math.floor(arr[i]-minValue)/bucketSize; buckets[index]=arrAppend(buckets[index], arr[i]); } int arrIndex = 0; for (int[] bucket : buckets) { if(bucket.length<=0)continue; //对每个桶进行排序,这里使用了快速排序 quickSort.quickSort(bucket, 0, bucket.length-1); for (int value : bucket) { arr[arrIndex++]=value; } } return arr; } /** * 自动扩容,并保存数据 * @param arr * @param value * @return */ private int[] arrAppend(int[] arr, int value) { arr = Arrays.copyOf(arr, arr.length+1); arr[arr.length-1] = value; return arr; } }
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零
从最低位开始,依次进行一次排序
从最低位排序一直到最高位(个位->十位->百位->…->最高位)排序完成以后, 数列就变成一个有序序列
需要我们获得最大数的位数,可以通过将最大数变为String类型,再求得它的长度即可
(注:图片来源:参考资料1)
换句话说,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
package pers.chh3213.sort; import java.util.Arrays; public class RadixSort { public static void main(String[] args) { RadixSort radix = new RadixSort(); int[] arr = {9, 160, -31, 25, -30, 49, 25, 21, 30}; radix.radixSort(arr); System.out.println(Arrays.toString(arr)); } /** * 获取数字位数 * @param num * @return */ protected int getNumLenght(long num) { // if (num == 0) { // return 1; // } // int lenght = 0; // for (long temp = num; temp != 0; temp /= 10) { // lenght++; // } // return lenght; //将最大值转为字符串,它的长度就是它的位数 int digits = (num + "").length(); return digits; } /** * 获取最大数的位数 * @param arr * @return */ private int getMaxDigit(int[] arr) { int max = arr[0]; for (int i : arr) { if(max<i)max=i; } return getNumLenght(max); } public int[] radixSort(int[] arr) { int mod = 10; int dev = 1; int maxDigit = getMaxDigit(arr); for (int i = 0; i < maxDigit; i++) { // 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10) int[][]counter = new int[mod*2][0]; for (int j = 0; j < arr.length; j++) { int bucket = ((arr[j]%mod)/dev)+mod; counter[bucket]=arrayAppend(counter[bucket], arr[j]); } int pos = 0; for (int[] bucket : counter) { for (int value : bucket) { arr[pos++]=value; } } dev*=10; mod*=10; } return arr; } /** * 自动扩容,并保存数据 * * @param arr * @param value */ private int[] arrayAppend(int[] arr, int value) { arr = Arrays.copyOf(arr, arr.length + 1); arr[arr.length - 1] = value; return arr; } }
OutOfMemoryError
。r[i]=r[j]
,且r[i]
在r[j]
之前,而在排序后的序列中,r[i]
仍在r[j]
之前,则称这种排序算法是稳定的:否则称为不稳定的这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
以上所有代码见于gitee仓库
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。