目录
一、冒泡排序(BubbleSort)
二、选择排序(SelectionSort)
三、插入排序(InsertionSort)
四、希尔排序(ShellSort)
五、归并排序(MergeSort)
六、快速排序(QuickSort)
七、堆排序(HeapSort)
八、计数排序(CountingSort)
九、桶排序(BucketSort)
十、基数排序(RadixSort)
写在前面的知识和总结:
- 稳定:有两个数a=b,a原本在b前面,而排序之后a仍然在b的前面;
- 不稳定:有两个数a=b,a原本在b前面,而排序之后a可能会出现在b的后面;
- 内排序:所有排序操作都在内存中完成;
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
- 时间复杂度: 一个算法执行所耗费的时间。
- 空间复杂度:运行完一个程序所需内存的大小。
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
一、冒泡排序(BubbleSort)
冒泡排序算法需要多次重头到尾地访问待排序的数列,每次都依次比较相邻的两个元素,如果他们的顺序错误就交换着两个元素,重复多次,直至某一次遍历没有交换,说明排序完成。
1.1代码实现:
public int[] BubbleSort(int[] data){
if(data==null || data.length==0){
return data;
}
int temp;
for(int i=0;i<data.length;i++){
for(int j=0;j<data.length-i-1;j++){
if(data[j]>data[j+1]){
temp = data[j];
data[j]=data[j+1];
data[j+1]=temp;
}
}
}
return data;
}
1.2性能分析
时间复杂度:平均O(n2) 最坏O(n2) 最好O(n)
空间复杂度:O(1)
二、选择排序(SelectionSort)
选择排序也是多次重头到尾地访问待排序的数列,整个数列可以看为有序区和无序区。第一次整个待排序的数列为无序区,有序区为空。每次选取一个最大或最小的数,放在待排序数列的开口或末尾。这样有序区的长度加一,无序区的长度减一。重复多次,直至无序区长度为零,所有元素都排序完毕。
2.1代码实现
public int[] SelectionSort(int[] data){
if(data==null || data.length==0){
return data;
}
for(int i=0;i<data.length;i++){
int miniumIndex = i;
for(int j=i+1;j<data.length;j++){
if(data[j]<data[miniumIndex]){
miniumIndex = j;
}
}
if(miniumIndex==i){
continue;
}else {
int temp = data[i];
data[i]=data[miniumIndex];
data[miniumIndex] = temp;
}
}
return data;
}
2.2性能分析
时间复杂度:最好O(n2) 最坏O(n2) 平均O(n2)
空间复杂度:O(1)
三、插入排序(InsertionSort)
插入排序是指在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。 按照此法对所有元素进行插入,直到整个序列排为有序。
3.1代码实现
public int[] InsertionSort(int[] data){
if(data==null || data.length==0 || data.length==1){
return data;
}
for (int i=1;i<data.length;i++){
int index=i;
int temp = data[i];
for(int j=i-1;j>=0;j--){
if(data[j]>temp){
data[j+1]=data[j];
index = j;
}else {
break;
}
}
data[index] = temp;
}
return data;
}
3.2性能分析
时间复杂度:平均O(n2) 最坏O(n2) 最好O(n)
空间复杂度:O(1)
四、希尔排序(ShellSort)
希尔排序是简单插入排序改进后的一种插入排序,也称为缩小增量排序。希尔排序是把待排序的数列按照一定的增量分成多组,对每组使用直接插入法排序。随着增量的逐渐减少,每组包含的数据越来越多,当增量减至1时,所有数据被分为一组,算法终止。
4.1过程演示
先取一个正整数d1<n,把所有相隔d1的记录放一组,组内进行直接插入排序。然后取d2<d1,重复上述分组和排序操作,直至di=1,即所有记录放进一个组中排序。
希尔排序的增量序列的选择与证明是个数学难题,我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,我们可以用一个序列来表示这种增量,{n/2,(n/2)/2...1},就称为增量序列。(我们选择这个增量序列是比较常用的,但其实并不是最优的。)
4.2代码实现
public int[] ShellSort(int[] array) {
int len = array.length;
int temp, gap = len / 2;
while (gap > 0) {
for (int i = gap; i < len; i++) {
temp = array[i];
int preIndex = i - gap;
while (preIndex >= 0 && array[preIndex] > temp) {
array[preIndex + gap] = array[preIndex];
preIndex -= gap;
}
array[preIndex + gap] = temp;
}
gap /= 2;
}
return array;
}
4.3性能分析
时间复杂度:平均O(n1.3) 最坏O(n2) 最好O(n)
空间复杂度:O(1)
五、归并排序(MergeSort)
将两个或两个以上的有序表组合成一个新的有序表叫归并。将两个有序表合并成一个有序表,称为2路归并。
2路归并排序:设初始序列含有n个记录,则可以看成n个有序的子序列,每个子序列的长度为1。两两合并,则得到n/2或(n/2)+1个长度为2或1的有序子序列,再两两合并,直到得到一个长度为n的有序序列。
5.1代码实现
public int[] MergeSort(int[] data){
if(data.length==0 || data.length==1){
return data;
}
int mid = data.length/2;
int[] data1=MergeSort(Arrays.copyOfRange(data,0,mid));
int[] data2=MergeSort(Arrays.copyOfRange(data,mid,data.length));
return merge(data1,data2);
}
public int[] merge(int[] data1,int[] data2){
int[] newData = new int[data1.length+data2.length];
int index = 0;
int p1 =0;
int p2=0;
while(p1<data1.length&&p2<data2.length){
if(data1[p1]<data2[p2]){
newData[index] = data1[p1];
index++;
p1++;
}else{
newData[index] = data2[p2];
index++;
p2++;
}
}
while(p1<data1.length){
newData[index] = data1[p1];
index++;
p1++;
}
while(p2<data2.length){
newData[index] = data2[p2];
index++;
p2++;
}
return newData;
}
5.2性能分析
时间复杂度:平均O(nlog2n) 最坏O(nlog2n) 最好O(nlog2n)
空间复杂度:O(n)
六、快速排序(QuickSort)
快速排序是迄今为止所有内排序算法中速度最快的一种。
基本思想是:从待排序数列中任取一个元素(例如取第一个)为中心,所有比他小的元素一律前放,比他大的元素一律后放,形成左右两个子表。然后再对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个,即为有序序列。
6.1代码实现
public int[] QuickSort(int[] data, int low, int high) {
if(low<high){
/*
Partition函数作用:
将小于中心元素的数放到前面,大于中心元素的数放到后面,且返回中心元素的位置,作为对左右子表继续递归排序的依据
*/
int index = Partition(data, low, high);
QuickSort(data, low, index-1);
QuickSort(data, index+1, high);
}
return data;
}
public int Partition(int[] data, int low, int high) {
//选取第一个数作为中心元素
int value = data[low];
while (low < high) {
while (data[high] > value && low<high) high--;
data[low] = data[high];
while (data[low] < value && low<high) low++;
data[high] = data[low];
}
data[low] = value;
return low;
}
6.2性能分析
每趟可以确定不止一个元素的位置,而且呈指数增加。
时间复杂度:平均O(nlog2n) 最坏O(n2) 最好O(nlog2n)
空间复杂度:O(nlog2n)
七、堆排序(HeapSort)
堆的定义:n个元素的序列(k1,k2,k3,...,kn)当且仅当满足下列关系时才称为堆。
ki<=k2i且ki<=k2i+1 或者 ki>=k2i且ki>=k2i+1 i=1,2,3,...n/2
堆排序: 将无序序列建成一个堆,得到关键字最小(最大)的记录,输出堆顶的最小(大)值后,使剩余的n-1个元素重新建成一个堆,则可得到n的元素的次小(大)值;重复执行,得到一个有序序列的过程。
堆排序需要解决的两个问题:
- 如何由无序序列建成一个堆?
- 输出堆顶元素后,如何调整剩余元素,使之成为一个新的最小堆或最大堆?
解决办法:如何由无序序列建成一个堆?
从无序序列的第n/2个元素(即此无序序列对应的完全二叉树的最后一个非终端节点)起,至第一个元素止,进行反复筛选。
解决办法:输出堆顶元素后,如何调整剩余元素,使之成为一个新的最小堆或最大堆?
输出堆顶元素后,以堆中最后一个元素替代之,然后将根节点与左右子树的根节点值进行比较,并与其中较小者进行交换;重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为“筛选”。
八、计数排序(CountingSort)
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。计数排序要求输入的数据必须是有确定范围的整数。
使用一个额外的数组C,其中第i个元素是待排序数组中值等于i的元素的个数,然后根据数组C来将A中的元素排到正确的位置。
8.1过程描述
- 找出待排序数组中的最大和最小的元素
- 统计待排序数组中每个值为i的元素出现的次数,存入数组C的第i项。数组中的每一个值,代表了数列中对应整数的出现次数。
- 根据统计结果输出排序结果:直接遍历数组C,输出元素的下标值(或者下标值加上偏移量),元素的值是几,就输出几次即可。
8.2代码实现
public int[] CountingSort(int[] data){
if (data ==null || data.length == 0 || data.length == 1) {
return data;
}
//找出待排序数组中的最大值和最小值
int maxValue=data[0],minValue=data[0];
for(int i=1;i<data.length;i++){
if(maxValue<data[i]){
maxValue=data[i];
}
if(minValue>data[i]){
minValue=data[i];
}
}
/*
开辟一个新的数组用来统计出现次数,数组长度为maxValue-minValue+1。
下标为0的代表最小值,下标为maxValue-minValue的代表最大值
因此后面反向填充的时候需要加上偏移量:minValue
*/
int[] counts = new int[maxValue-minValue+1];
for(int i=0;i<data.length;i++){
counts[data[i]-minValue]++;
}
//反向填充待排序数组
int index = 0;
for(int i=0;i<counts.length;i++){
//counts[i]是几就填充几个i(需要加上偏移量)
for(int j=0;j<counts[i];j++){
data[index] = i+minValue;
index++;
}
}
return data;
}
8.3性能分析
时间复杂度:平均O(n+k) 最坏O(n+k) 最好O(n+k)
空间复杂度:O(n+k)
计数排序是一种牺牲内存空间来换取低时间复杂度的排序算法,优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k),其中k是整数的范围,快于任何比较排序算法。而当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)),如归并排序,堆排序)。计数排序对于数据范围很大的数组,需要大量时间和内存。
九、桶排序(BucketSort)
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能使用别的排序算法或是以递归方式继续使用桶排序),最后依次把各个桶中的记录列出来即得到有序序列。
9.1过程描述
- 人为设置一个BucketSize,作为每个桶所能放置多少个不同数值(例如当BucketSize==5时,该桶可以存放{1,2,3,4,5}这几种数字,但是容量不限,可以存放100个3);
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序(如果递归使用桶排序为各个桶排序,则当桶数量为1时要手动减小BucketSize增加下一循环桶的数量,否则会陷入死循环,导致内存溢出。)
- 从不是空的桶里把排好序的数据拼接起来。
9.2代码实现
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;
}
9.3性能分析
时间复杂度:平均O(n+k) 最坏O(n2) 最好O(n)
空间复杂度:O(n+k)
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。桶划分的越小,各个桶的数据越少,排序所用的时间也会越少,但相应的空间消耗就会增大。
十、基数排序(RadixSort)
基数排序、计数排序、桶排序这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异。
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;
基数排序有两种方法:MSD 从高位开始进行排序,LSD 从低位开始进行排序
基数排序是从低位开始将待排序的数按照这一位的值放到相应的编号为0~9的桶中。等到低位排完得到一个子序列,再将这个序列按照次低位的大小进入相应的桶中,一直排到最高位为止,数组排序完成。
10.1过程演示
10.2代码实现
public static int[] RadixSort(int[] array) {
if (array == null || array.length < 2)
return array;
//找出最大数,并取得位数,用来确定分配和收集的次数;
int max = array[0];
for (int i = 1; i < array.length; i++) {
max = Math.max(max, array[i]);
}
int maxDigit = 0;
while (max != 0) {
max /= 10;
maxDigit++;
}
int mod = 10, div = 1;
//分配十个桶(ArrayList),每个桶里面用ArrayList存储多个数
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++)
bucketList.add(new ArrayList<Integer>());
//一次分配和收集
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
//将每个数分配到桶中
for (int j = 0; j < array.length; j++) {
int num = (array[j] % mod) / div;
bucketList.get(num).add(array[j]);
}
//收集,收集完成后清空桶便于下次收集。
int index = 0;
for (int j = 0; j < bucketList.size(); j++) {
for (int k = 0; k < bucketList.get(j).size(); k++)
array[index++] = bucketList.get(j).get(k);
bucketList.get(j).clear();
}
}
return array;
}
10.3性能分析
时间复杂度:平均O(n*k) 最坏O(n*k) 最好O(n*k)
空间复杂度:O(n+k)
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。