总结
十种常见排序算法可以分为两大类:
比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
排序算法的稳定
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
为啥要考虑算法的稳定性呢,稳定算法在单次排序的时候,意义并不显著,虽然可以减少元素交换,其实链表是可以避免这个消耗的,只不过操作比较复杂,其意义显示在基数排序中,即,我们要对多个关键词多次排序,这个时候,就一定要使用稳定算法。举一个现实的例子,比如排序的对象是人名,假设有以下两个人名:
Smith, Alfred
Smith, Zed
我们先按first name排序,再按照last name排序,按照first name排序完成以后,就是上面的样子,再去按照last name排序,如果算法不稳定,则顺序极就会颠倒,这里的last name和first name完全可以抽象成基数排序的不同位,不是稳定算法,就不能得到正确结果。
冒泡排序、选择排序、插入排序都是比较简单的排序,代码比较简单,而且不需要额外空间,但是时间都是O(n2),其中冒泡排序、插入排序是交换相邻两个元素,是稳定的,选择排序是跳着交换的,所以不稳定。希尔排序是最先打破O(n2)的,希尔排序的时间复杂度是取决于增量序列的选择。堆排序和归并排序从理论上来讲时间复杂度都是最好的,归并排序需要额外空间,堆排序不用,归并排序是稳定的,堆排序不稳定,而且实际应用中堆排序隐含的常数因子很大,并不是很快。快速排序在实际应用中隐含常数因子非常小,速度是最快的。基数排序、计数排序都属于桶排序,满足一定的条件时可以达到线性的时间复杂度。
时间复杂度
1.冒泡排序
需要经过两轮,外边的循环决定需要进行的轮次,内层循环是比较大小,如果当前位置比后面位置大,则交换位置,否则,不交换位置,继续检索下一个位置能否交换,直到达到已经确定位置的数。
可以稍微改进一下,用一个标记量 来判断每一轮是否有数据交换,如果这一轮没有数据交换的话,就不进行后面的轮次了。
public class BubbleSort {
public static void main(String[] args) {
int[] arr = new int[] { 12, 36, 20, 18, 96, 54, 32, 66, 54, 20 };
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
2.选择排序
先是找出最小的数字的索引,让它和第一个位置的数字交换位置,然后再从剩下的数字中找最小的,和第二个位置的数字交换位置,这里面在找到当前轮次中的最小值后再进行交换位置,可用一个索引来记录最小位置的索引。
public class ChooseSort {
public static void main(String[] args) {
int arr[] = new int[9];
for (int i = 0; i < 9; i++) {
arr[i] = (int) (Math.random() * 90 + 10);
System.out.print(arr[i] + "\t");
}
System.out.println();
for (int j = 0; j < arr.length; j++) {
int k = j;
for (int i = j + 1; i < arr.length; i++) {
if (arr[i] < arr[k]) {
k = i;
}
}
int temp = arr[j];
arr[j] = arr[k];
arr[k] = temp;
}
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
3.插入排序
插入排序就是对于当前的数字,我们从后往前去找,如果前面一个位置的数字比当前数字大,就把该位置的数字往后挪,直到前面的数字比当前数字小的时候,就把当前位置的数字填到该位置中。
public class InsertSort {
public static void main(String[] args) {
int arr[] = new int[9];
for (int i = 0; i < 9; i++) {
arr[i] = (int) (Math.random() * 90 + 10);
System.out.print(arr[i] + "\t");
}
System.out.println();
for(int i = 1; i < arr.length;i++) {
int preIndex = i-1;
int current = arr[i];
while(preIndex >= 0 && arr[preIndex]>current) {
//把当前值与前面位置的所有位置进行比较,如果当前值比前面一个位置的数字大,就把前面一个数的
arr[preIndex + 1] = arr[preIndex];
//位置往后挪一位,然后再比较当前值与前前一位比较,直到找到自己的位置。在找的过程中都在挪位置
preIndex--;
}
//找到数字的正确位置的,把数字放回位置上
arr[preIndex + 1] = current;
}
}
}
4 希尔排序
希尔排序我理解的就是加强版的插入排序,先是进行分组,按照一定的间隔进行分组,对于每个分组的组内进行插入排序,直到分组的间隔为1。
package contact;
public class ShellSort {//希尔排序就是先分组,按照一定的间隔进行分组,对于每个分组内的采取插入排序,
//分组的间隔逐渐减小,最后减少到1
public static void main(String[] args) {
int arr[] = new int[9];
for (int i = 0; i < 9; i++) {
arr[i] = (int) (Math.random() * 90 + 10);
System.out.print(arr[i] + "\t");
}
int len = arr.length;
//这里需要先分组,gap表示同一组元素的分组间隔,例如需要排序的数组一共有8个,刚开始的分组间隔就是4
//意思就是需要把数字分为四个组,第0个和第4个为一组,第1个和第5个为一组,...,在组内进行插入排序
//第一轮过后改变分组间距,此时分组间距为2,也就是第0,2,4,6这几个数字一组,其余数字一组,一共有两组
//再分别对这两组的数字进行插入排序
for(int gap = (int) Math.floor(len/2);gap > 0;gap = (int) Math.floor(gap/2)) {
for(int i = gap; i< len ;i++) {//这里并不是一个组一个组的进行插入的,是交替组进行插入的
int j = i,current = arr[i];
while(j-gap>=0&¤t<arr[j-gap]) {//这里和插入排序十分类似,如果把这个gap换成1的话
//就是插入排序了
arr[j]=arr[j-gap];
j = j - gap;
}
arr[j]=current;
}
}
for(int i=0;i<arr.length;i++) {
System.out.println(arr[i]);
}
}
}
5.归并排序
归并排序就是一个递归的排序,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
package contact;
import java.util.Arrays;
public class MergeSort {
public static void main(String[] args) {
int arr[] = new int[9];
for (int i = 0; i < 9; i++) {
arr[i] = (int) (Math.random() * 90 + 10);
System.out.print(arr[i] + "\t");
}
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int[] arr) {
int[] temp = new int[arr.length];// 在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
sort(arr, 0, arr.length - 1, temp);
}
private static void sort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2;
sort(arr, left, mid, temp);// 左边归并排序,使得左子序列有序
sort(arr, mid + 1, right, temp);// 右边归并排序,使得右子序列有序
merge(arr, left, mid, right, temp);// 将两个有序子数组合并操作
}
}
private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left;// 左序列指针
int j = mid + 1;// 右序列指针
int t = 0;// 临时数组指针
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[t++] = arr[i++];
} else {
temp[t++] = arr[j++];
}
}
while (i <= mid) {// 将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while (j <= right) {// 将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
// 将temp中的元素全部拷贝到原数组中
while (left <= right) {
arr[left++] = temp[t++];
}
}
}
6.快速排序
快速排序算法是20世纪十大算法之一,十分经典,运行速度很快,可惜不稳定。
先从数列中取出一个数作为基准数,分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。再对左右区间重复第二步,直到各区间只有一个数。个人认为,快速排序的核心就是找到轴数据的确定索引,因为序列有n个,所以需要找n次,在找的过程中会划分成logn个子序列,所以其时间复杂度为nlogn。另一种理解思路,就是把轴数据(第一个数)拿出来,这样就有了空位,我们从后面找找看有没有比轴数据小的数,小的话就填到刚刚拿出来那个数字的空里面,这样的话后面就也有一个空了,此时就要从前面看看有没有数字比轴数据大的,有的话就把它刚刚拿出数据后的空里面,这样前后交替,直到前后的索引相等,此时空出来的位置就是轴数据的位置,轴数据前面的数字都比轴数据小,轴数据后面的数字都比轴数据大,此时轴数据的位置就固定下来了,没有必要再进行排序了,就下来就以轴数据为划分,前面为一个子序列,后面也是一个子序列,再次采用这样的方法找每个子序列中轴数据的正确索引,直到每个数据的索引都被确定。
这里选取轴数据每次都是选取的第一个数,这样并不合理,有人提出了一种三数取中的方法,就是在一个序列中,选在前面个数字,和最后面个数字和最中间个数字,就是索引分别为,low,high和mid的三个数字进行比较,选取大小在中间的那个数字作为轴数据,这样可以保证不会选到最大或者最小的那个数字作为轴数据了
package contact;
public class QuickSort {
public static void main(String[] args) {
int arr[] = new int[10];
for (int i = 0; i < 10; i++) {
arr[i] = (int) (Math.random() * 9000 + 10);
System.out.print(arr[i] + "\t");
}
//long startTime=System.nanoTime();
quickSort(arr, 0, arr.length - 1);
//long endTime=System.nanoTime(); //获取结束时间
//System.out.println("程序运行时间:" + (endTime - startTime) + "ns"); //输出程序运行时间
for (int i : arr) {
System.out.println(i);
}
}
private static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 找寻基准数据的正确索引
int index = getIndex(arr, low, high);
// 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
quickSort(arr, low, index - 1);
quickSort(arr, index + 1, high);
}
}
private static int getIndex(int[] arr, int low, int high) {//相当于是找到轴数据的索引,每个数据的索引确定一次,所以为n,划分子序列为logn
// 基准数据,每次选择第一个数据为基准数据
int tmp = arr[low];//相当于把队首这个元素抽出来,这里就有一个空位了
while (low < high) {
// 当队尾的元素大于等于基准数据时,向前挪动high指针
while (low < high && arr[high] >= tmp) {
high--;
}
//这里其实是交替进行的,先在队尾开始找,找到后交换位置,又从队头开始找
// 如果队尾元素小于tmp了,需要将其赋值给low
arr[low] = arr[high];//这里就是填坑,就是找到小于轴数据的数字之后就把它放在先前空出来的位置上,这样的话high这个位置又是空的了
//从后面找到一个数字后就开始从前面的指针开始找了,就是想找到一个大的数字放在刚刚的空位上
// 当队首元素小于等于tmp时,向前挪动low指针
while (low < high && arr[low] <= tmp) {
low++;
}
// 当队首元素大于tmp时,需要将其赋值给high
arr[high] = arr[low];
}
// 跳出循环时low和high相等,此时的low或high就是tmp的正确索引位置
// 由原理部分可以很清楚的知道low位置的值并不是tmp,所以需要将tmp赋值给arr[low]
arr[low] = tmp;//这里low和high都是一样大的
return low; // 返回tmp的正确位置,就是返回了轴数据的正确索引!然后把轴数据前面的分为一个子序列,后面的也分为一个子序列。
}
}
快速排序的图里面,黄颜色的数据就是轴数据pivot。
7.堆排序
将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
package contact;
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
int[] arr = { 9, 8, 7, 6, 5, 4, 3, 2, 1 };
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int[] arr) {
// 1.构建大顶堆
for (int i = arr.length / 2 - 1; i >= 0; i--) {
// 从第一个非叶子结点从下至上,从右至左调整结构,这样调整是因为有的双亲节点调整后下面的双亲节点顺序也可能会乱,
adjustHeap(arr, i, arr.length);
}
// 2.调整堆结构+交换堆顶元素与末尾元素
for (int j = arr.length - 1; j > 0; j--) {
swap(arr, 0, j);// 将堆顶元素与末尾元素进行交换
adjustHeap(arr, 0, j);// 重新对堆进行调整
}
}
/**
* 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
*
* @param arr
* @param i
* @param length
*/
public static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i];// 先取出当前元素i
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {// 从i结点的左子结点开始,也就是2i+1处开始
if (k + 1 < length && arr[k] < arr[k + 1]) {// 如果左子结点小于右子结点,k指向右子结点
k++;//这里比较巧妙,因为需要把两个子节点和双亲节点中最大的那个放在双亲节点的位置,这里先是选出较大的子节点,再让其与双亲节点进行比较
}
if (arr[k] > temp) {// 如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
arr[i] = arr[k];
i = k;
} else {
break;
}
}
arr[i] = temp;// 将temp值放到最终的位置
}
/**
* 交换元素
*
* @param arr
* @param a
* @param b
*/
public static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
8.计数排序
计数排序就是字面意思,数一数那个数字出现的次数,计数排序有一些局限,首先只适用于整数,而且还需要占用空间,当数据比较集中时候比较适合。整个算法的过程是:先找出整个数列中的最大值和最小值,然后开辟一个长度为max-min+1的数组,命名为count,
package contact;
public class CountSort {
public static void main(String[] args) {
int arr[] = new int[10];
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i = 0; i < 10; i++) {
arr[i] = (int) (Math.random() * 9000 + 10);
System.out.print(arr[i] + "\t");
}
for (int num : arr) {
max = Math.max(max, num);
min = Math.min(min, num);
}
// 初始化计数数组count
// 长度为最大值减最小值加1
int[] count = new int[max - min + 1];// 此时count数组每个元素值为0
// 对计数数组各元素赋值
for (int num : arr) {
// arr中的元素要减去最小值,再作为新索引
count[num - min]++;
}
// 计数数组变形,新元素的值是前面元素累加之和的值
for (int i = 1; i < count.length; i++) {
count[i] += count[i - 1];
}
// 创建结果数组
int[] result = new int[arr.length];
// 遍历A中的元素,填充到结果数组中去
for (int j = 0; j < arr.length; j++) {
result[count[arr[j] - min] - 1] = arr[j];//这一步有点厉害!这样的话使得排序稳定,有点搞不清楚,https://www.cnblogs.com/xiaochuan94/p/11198610.html
count[arr[j] - min]--;
}
}
}
9.桶排序
桶排序其实是基数排序的普通版,基数排序就是设置的桶的个数为max-min+1,而桶排序的话设置桶的个数是自己确定的。计数排序主要解决的是整数,桶排序可以解决浮点数的排序。步骤为:先找出数组的最大值和最小值,然后确定桶的个数,这个桶的个数可以根据数据的分布来确定,如果是浮点数的话,可以采用数据的个数来当作桶的个数。之后遍历原数组,确定每个数属于哪个桶,并将其放入对应的桶里面,然后对每个桶的数据进行排序(用任何一种排序算法,快排比较好),最后将每个桶的数据有序放入原数组。
package contact;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class BucketSort {
/**
* 桶排序
* @param arr
* @return
*/
public static double[] bucketSort(double[] arr){
//1.计算出最大值和最小值,求出两者的差值
double min = arr[0];
double max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (max < arr[i]){
max = arr[i];
}
if (arr[i] < min){
min = arr[i];
}
}
double d = max - min;
//2.初始化桶
int bucketNum = arr.length;//桶的数量这个地方很重要,这里因为数据是浮点数,所以桶的数量就是数组的长度,建议这个地方设置成用户输入的,用户根据情况设置。
List<LinkedList<Double>> bucketList = new ArrayList<>(bucketNum);
for (int i = 0; i < bucketNum; i++) {
bucketList.add(new LinkedList<>());
}
//3.遍历数组中的元素,把所有元素都放入对应的桶当中
for (int i = 0; i < arr.length; i++) {
//计算当前元素应该放在哪个桶里面
int num = (int)((arr[i] - min) / (d / (bucketNum - 1)));//这里d/(bucketNum-1)就是单份桶的容量,看看当前这个数字属于哪个桶
bucketList.get(num).add(arr[i]);
}
//4.对每个桶里面的元素进行排序
for (int i = 0; i < bucketNum; i++) {
Collections.sort(bucketList.get(i));
}
//5.输出全部元素
int k = 0;
for(LinkedList<Double> doubles : bucketList){
for (Double aDouble : doubles) {
arr[k] = aDouble;
k++;
}
}
return arr;
}
public static void main(String[] args) {
double[] arr = new double[]{4.12, 6.421, 0.0023, 3.0, 2.123, 8.122, 4.12, 10.09};
System.out.println(Arrays.toString(arr));
arr = bucketSort(arr);
System.out.println(Arrays.toString(arr));
}
}
10.基数排序
基数排序就是先按照个位数字进行计数排序,排好后放回数组,再按照十位排序,排好后放回,直到所有数字中的最大那个数字的最高位都排序过了,排序就结束了。
package contact;
import java.util.Arrays;
public class RadixSort {
// 基于计数排序的基数排序算法
public static void radixSort(int[] array, int radix, int digit) {
// array为待排序数组
// radix,代表基数,实际就是几个数字,那就是10
// digit代表排序元素的位数,实际意义是排序趟数,就是最大那个数字的位数
int length = array.length;
int[] res = new int[length];
int[] c = new int[radix];// radix就是10,因为0到9共10个数字
int divide = 1;// 用于每次把数字缩小10倍
for (int i = 0; i < digit; i++) {
res = Arrays.copyOf(array, length);
Arrays.fill(c, 0);
//按照基数,分别将这些数放到那10个桶里面
for (int j = 0; j < length; j++) {
int tempKey = (res[j] / divide) % radix;
c[tempKey]++;
}
//修改计数数组的值为累加值
for (int j = 1; j < radix; j++) {
c[j] += c[j - 1];
}
//进行一轮后将数字重新放回数组
for (int j = length - 1; j >= 0; j--) {
int tempKey = (res[j] / divide) % radix;
array[c[tempKey] - 1] = res[j];
c[tempKey]--;
}
//修改进行轮数的
divide = divide * radix;
}
}
//取最大的那个数字,看看有好多位,有好多位就要计数好多趟,返回的就是趟数
public static int countDigit(int[] array) {
int max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
int time = 0;
while (max > 0) {
max /= 10;
time++;
}
return time;
}
public static void main(String[] args) {
int[] array = { 3, 2, 3, 2, 5, 333, 45566, 2345678, 78, 990, 12, 432, 56 };
int time = countDigit(array);
// System.out.println(time);
radixSort(array, 10, time);
for (int i = 0; i < array.length; i++) {
System.out.print(" " + array[i]);
}
}
}