排序算法比较
时间复杂度
适用场景
-
若 n 较小 (如 n ≤ 50 ),可采用直接插入或直接选择排序。
当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。 -
若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜;
-
若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
若要求排序稳定,则可选用归并排序。通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定 的,所以改进后的归并排序仍是稳定的。
1.冒泡排序
依次比较两个相邻的元素列,顺序错误就进行交换,直到排序完成。每次循环都将最大的数值选出来,浮到队列的顶端。平均时间复杂度为 O(n 2),最好情况为 O(n)。
public class BubbleSort {
public void bubbleSort(int[] a) {
for (int i = 1; i < a.length; i++) {
for (int j = 0; i < a.length - i; j++) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
//优化的冒泡排序,如果 flag 没有变化,说明数组没有发生交换,已经有序,可以直接跳出循环...
public void bubbleSort1(int[] a) {
for (int i = 1; i < a.length; i++) {
int flag = 0;
for (int j = 0; j < a.length - i; j++) {
if (a[j] > a[j + 1]) {
flag = 1;
int temp = a[j + 1];
a[j + 1] = a[j];
a[j] = temp;
}
}
if (flag == 0) break;
}
}
}
2.选择排序
对待排序的序列进行 n-1 遍处理,第 i 遍处理将 i - n 的最小者与 i 位置数值交换,时间复杂度为 O(n 2)。
class ChooseSort{
public void chooseSort(int[] a) {
for(int i = 0; i < a.length; i++) {
int minIndex = i;
for(int j = i + 1; j < a.length; j++) {
if(a[j] < a[i])
minIndex = j;
}
if(i != minIndex) {
swap(i,minIndex);
}
}
}
}
3.插入排序
插入排序算法:
- 以数组的某一位作为分隔位,比如 i=1,假设左面的都是有序的;
- 将 i 位的数据拿出来,放到临时变量里,这时 i 位置就空出来了;
- 从 j = i -1 开始将左面的数据与当前 i 位的数据(即 temp)进行比较,如果 array[j] > temp,则将 array[j] 后移一位,即 array[j+1] = array[j],此时 j 就空出来了再用 i-2 (即 j-1)位的数据和 temp 比,重复步骤3;
- 直到找到 <= temp 的数据或者比到了最左面(说明 temp 最小),停止比较,将 temp放在当前空的位置上;
- i 向后挪1,即 i = i + 1,temp = array[i],重复步骤 2-4 直到 i = array.length,排序结束;
- 此时数组中的数据即为从小到大的顺序.
public class InsertSort {
public void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
// 从外出向右的 i 作为比较对象数据的 i
int temp = arr[i];
int j = i - 1;
// 当比到最左边或者遇到比当前 temp 小的数据时 结束循环
while (j >= 0 && arr[j] > temp) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = temp;
}
}
}
4.希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录 “基本有序” 时,再对全体记录进行依次直接插入排序。
平均时间复杂度为 O(nlogn),最坏为 O(n^2),即 O(n^(1.3~2))
空间复杂度为 O(1)
public class ShellSort {
public void shellSort(int[] a) {
int N = a.length;
for (int gap = N / 2; gap > 0; gap /= 2) {
for (int i = gap; i < N; i++) {
int temp = a[i];
int j = i - gap;
while (j >= 0 && a[j] > temp) {
a[j + gap] = a[j];
j -= gap;
}
a[j + gap] = temp;
}
}
}
}
5.快速排序
选择一个基准元素,然后两个游标,一首一尾,然后逐个比较,经过一轮比较后,一部分比基准小,另一部分比基准大,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。
按从小到大排:当首游标位置的元素比基准小则把起始游标向前后一位start++,当尾游标比基准大时则把尾游标向前移end–;如果不符合则互换,每找到一个不符合的就赋值一次,等最后在把基准放给那个正确的位置,然后再对基准的前一部分和后一部分递归。
public class QuickSort {
public void quickSort(int[] arr, int left, int right) {
if (left < right) {
int mid = partiton(arr, left, right);
quickSort(arr, 0, mid - 1);
quickSort(arr, mid + 1, right);
}
}
private int partiton(int[] arr, int left, int right) {
int temp = arr[left];
while (left < right) {
while (left < right && arr[right] >= temp) right--;
arr[left] = arr[right];
while (left < right && arr[left] <= temp) left++;
arr[right] = arr[left];
}
arr[left] = temp;
return left;
}
}
非递归写法:
private static void quickSort(int[] a, int start, int end) {
LinkedList<Integer> stack = new LinkedList<Integer>(); // 用栈模拟
if (start < end) {
stack.push(end);
stack.push(start);
while (!stack.isEmpty()) {
int l = stack.pop();
int r = stack.pop();
int index = partition(a, l, r);
if (l < index - 1) {
stack.push(index - 1);
stack.push(l);
}
if (r > index + 1) {
stack.push(r);
stack.push(index + 1);
}
}
}
}
private static int partition(int[] a, int start, int end) {
int pivot = a[start];
while (start < end) {
while (start < end && a[end] >= pivot) end--;
a[start] = a[end];
while (start < end && a[start] <= pivot) start++;
a[end] = a[start];
}
a[start] = pivot;
return start;
}
}
6.归并排序
public class MergeSort {
public void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = (left + right) >> 1;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
mege(arr, left, mid, right);
}
}
private void mege(int[] arr, int left, int mid, int right) {
int[] tempArr = new int[right - left + 1];
int i = left, j = mid + 1;
int k = 0;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) tempArr[k++] = arr[i++];
else tempArr[k++] = arr[j++];
}
while (i <= mid) tempArr[k++] = arr[i++];
while (j <= right) tempArr[k++] = arr[j++];
for (k = 0; k < tempArr.length; k++)
arr[k + left] = tempArr[k];
}
}
7.堆排序
public class HeapSort {
public void heapSort(int[] arr) {
int N = arr.length - 1;
// 从最后的子堆开始建堆
for (int k = N / 2; k >= 0; k--) {
sink(arr, k, N);
}
// 堆排序
for (int i = N; i >= 0; i--) {
swap(arr, 0, i);
sink(arr, 0, i - 1);
}
}
// 调整堆,子节点为 2n+1 和 2n+2
private void sink(int[] arr, int k, int N) {
while (2 * k + 1 <= N) {
int i = 2 * k + 1;
if (i < N && arr[i] < arr[i + 1]) i++;
if (arr[k] >= arr[i]) break;
swap(arr, k, i);
k = i;
}
}
private void swap(int[] arr, int x, int y) {
int temp = arr[x];
arr[x] = arr[y];
arr[y] = temp;
}
}
8.计数排序
计数排序是一种非比较性质的排序算法,元素从未排序状态变为已排序状态的过程,是由额外空间的辅助和元素本身的值决定的。计数排序过程中不存在元素之间的比较和交换操作,根据元素本身的值,将每个元素出现的次数记录到辅助空间后,通过对辅助空间内数据的计算,即可确定每一个元素最终的位置。
- 根据待排序集合中最大元素和最小元素的差值范围,申请额外空间;
- 遍历待排序集合,将每一个元素出现的次数记录到元素值对应的额外空间内;
- 对额外空间内数据进行计算,得出每一个元素的正确位置;
- 将待排序集合每一个元素移动到计算得出的正确位置上。
时间复杂度为 O(n+m),空间复杂度为 O(m),其中 m 为数据规模的大小
public class CountSort {
public void countSort(int[] arr) {
if (arr == null || arr.length == 0) return;
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
//找出数组中的最大最小值
for (int num : arr) {
max = Math.max(max, num);
min = Math.min(min, num);
}
int[] map = new int[max - min + 1];
//找出每个数字出现的次数
for (int num : arr) map[num - min]++;
int index = 0;
for (int i = 0; i < map.length; i++) {
while (map[i]-- > 0) {
arr[index++] = i + min;
}
}
}
}
9.桶排序
- 根据待排序集合中最大元素和最小元素的差值范围和映射规则,确定申请的桶个数;
- 遍历待排序集合,将每一个元素移动到对应的桶中;
- 对每一个桶中元素进行排序,并移动到已排序集合中。
最简单的是将值为 i 的元素放入 i 号桶,最后再将桶中的元素倒出来。
时间复杂度 O(n),空间复杂度为 O(m),m为数组值的范围
public class Main {
public void bucketSort(int[] arr) {
// 新建一个桶的集合
List<Integer>[] buckets = new LinkedList[xxx];
for (int i = 0; i < 10; i++) {
// 新建一个桶,并将其添加到桶的集合中去。
// 由于桶内元素会频繁的插入,所以选择 LinkedList 作为桶的数据结构
buckets.add(new LinkedList<Float>());
}
// 将输入数据全部放入桶中并完成排序
for (float data : arr) {
int index = getBucketIndex(data);
insertSort(buckets.get(index), data);
}
// 将桶中元素全部取出来并放入 arr 中输出
int index = 0;
for (LinkedList<Float> bucket : buckets) {
for (Float data : bucket) {
arr[index++] = data;
}
}
}
/**
* 计算得到输入元素应该放到哪个桶内
*/
public static int getBucketIndex(float data) {
// 这里例子写的比较简单,仅使用浮点数的整数部分作为其桶的索引值
// 实际开发中需要根据场景具体设计
return (int) data;
}
/**
* 我们选择插入排序作为桶内元素排序的方法 每当有一个新元素到来时,我们都调用该方法将其插入到恰当的位置
*/
public static void insertSort(List<Float> bucket, float data) {
ListIterator<Float> it = bucket.listIterator();
boolean insertFlag = true;
while (it.hasNext()) {
if (data <= it.next()) {
it.previous(); // 把迭代器的位置偏移回上一个位置
it.add(data); // 把数据插入到迭代器的当前位置
insertFlag = false;
break;
}
}
if (insertFlag) {
bucket.add(data); // 否则把数据插入到链表末端
}
}
}
10.基数排序
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
基数排序是内排序中性格比较怪异的一种,它无需比较和交换,而是按位分配和收集。通俗地讲就是,对各元素先按个位上的数值排序,接着十位,百位……直到最大元素的最高位,从而排序完成。具体算法如下
- 求最大位数。由于基数排序是按位排序,所以先要确定最大位数,才能知道排序的趟数。求最大位数的方法是求最大元素的最高位,用循环做比较简单。
- 分配。由于每位的取值范围是0-9,因此需要十个容器来装,我们一般用十个队列即可,这十个队列标号为0-9。对于每一趟,我们取每一个元素在该位的数值依次入队。
- 收集。在一趟排序完成后,我们按顺序从0-9队列中依次出队收集元素。
- 继续进行分配和收集,直到最大位数排序完成。
平均时间复杂度 O (nlog®m),其中 n 为数据规模,r 为所采取的基数,而 m 为堆数