排序的介绍
排序也称排序算法,排序是将一组数据,依指定的顺序进行排列的过程。
排序的分类
- 内部排序:指需要处理的所有数据都加载到内存存储器中进行排序。
- 外部排序:由于数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。
算法的时间复杂度
时间频度
一个算法花费的时间与算法中语句的执行次数成正比,哪个算法中语句执行次数多,它花费的时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
随着n的变大,可以忽略:
- 常数项可以忽略。如 T(n) = 2n + 20 和 T(n) = 2n。
- 低次项可以忽略。如T(n) = n2+3n+10 和 T(n) = n2
- 系数可以忽略。如T(n) = 3n2 和 T(n) = 5n2
时间复杂度
- 一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示。若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度,简称时间复杂度。
- T(n)不同,当时间复杂度可能相同。如T(n)=n2+7n+6 与 T(n)=3n2+2n+2它们的T(n)不同 ,但时间复杂度相同,都为O(n2)
- 计算时间复杂度的方法:如 T(n)=3n2+2n+2
- 用常数1代替运行中的所有加法常数。T(n)=3n2+2n+2 => T(n)=3n2+2n+1
- 修改后的函数,只保留最高阶项。T(n)=3n2+2n+1 => T(n)=3n2
- 去除最高阶项的系数。T(n)=3n2 => T(n)=n2
常见的时间复杂度
- 常数阶O(1)
- 对数阶O(log2n)
- 线性阶O(n)
- 线性对数阶O(nlog2n)
- 平方阶O(n2)
- 立方阶O(n3)
- k次方阶O(nk)
- 指数阶O(2n)
算法的平均时间复杂度和最坏时间复杂度
- 平均时间复杂度是指所有可能的输入实例以等概率出现的情况下,该算法的运行时间。
- 最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。这样做的原因是:最坏情况下的时间复杂度是在算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
- 平均时间复杂度和最坏时间复杂度是否一致,和算法相关。
排序发 | 平均时间 | 最差情形 | 稳定度 | 额外空间 | 备注 |
---|---|---|---|---|---|
冒泡排序 | O(n2) | O(n2) | 稳定 | O(1) | n小时,算法效率好 |
选择排序 | O(n2) | O(n2) | 不稳定 | O(1) | n小时,算法效率好 |
插入排序 | O(n2) | O(n2) | 稳定 | O(1) | 大部分已经排好序时,算法效率好 |
基数排序 | O(logRB) | O(logRB) | 稳定 | O(n) | B为真数(0~9) R为基数(个十百) |
希尔排序 | O(nlogn) | O(ns)(1<s<2) | 不稳定 | O(1) | n是所选分组 |
快速排序 | O(nlogn) | O(n2) | 不稳定 | O(nlogn) | n大时,算法效率较好 |
归并排序 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n大时,算法效率较好 |
堆排序 | O(nlogn) | O(nlogn) | 不稳定 | O(1) | n大时,算法效率较好 |
算法空间复杂度
类似于时间复杂度,一个算法的空间复杂度定义 为该算法所耗费的存储空间,它也是问题规模n的函数。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的度量。有点算法需要占用的临时工作单元与解决问题的规模n有关,它随着n的增大而增大。当n较大时,将占用较多的存储单元。例如快速拍下下和归并排序。
在做算法分析时,主要讨论算法的时间复杂度。从用户体验的角度来看,更看重程序执行的速度。一些缓存产品(redis、memcache)和算法(基数排序)本质上都是空间换时间。
排序算法介绍
冒泡排序
基本介绍
冒泡排序(Bubble Sorting)的基本思想是:通过从前向后(从下表较小的元素开始)对待排序序列,依次比较相邻元素的值。若发现逆序则交换,使值较大的元素逐渐从前向后移动。就像水底下的气泡一样逐渐向上冒。
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行交换。从而减少不必要的比较。
代码实现
import java.util.Scanner;
public class BubbleSort {
public static void main(String[] args) {
int n;
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; ++i) {
arr[i] = scanner.nextInt();
}
bbubbleSort(arr);
print(arr);
}
public static void bbubbleSort(int[] arr) {
boolean flag = false;
for (int i = 1; i < arr.length; ++i) {
for (int j = 0; j < arr.length -i; ++j) {
if (arr[j] < arr[j+1]) {
flag = true;
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
if (!flag) {
break;
}
flag = false;
}
}
public static void print(int[] arr) {
for (int i = 0; i < arr.length; ++i) {
System.out.printf("%d ", arr[i]);
}
System.out.println();
}
}
选择排序
基本介绍
选择排序也属于内部排序,是从欲排序的数据中,按照指定规则选出来某一元素,再依规定交换位置后达到排序的目的。
选择排序思想
选择排序(select sorting)也是一种简单的排序方法。它的基本思想是:
- 第一次从arr[0]~arr[n-1]中选取最小值,与arr[0]交换;
- 第二次从arr[1]~arr[n-1]中选取最小值,与arr[1]交换;
- 第i次从arr[i-1]~arr[n-1]中选择最小值,与arr[i-1]交换;
- 直到第n-1次,arr[n-2]~arr[n-1]中选择最小值,与arr[n-2]交换;
- 总共经过n-1次,得到一个排序
代码实现
import java.util.Scanner;
public class SelectSort {
public static void main(String[] args) {
int n;
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; ++i) {
arr[i] = scanner.nextInt();
}
selectSort(arr);
print(arr);
}
public static void selectSort(int[] arr) {
for (int i =0 ; i < arr.length - 1; ++i) {
int min = arr[i], k = i;
for (int j = i + 1; j < arr.length; ++j) {
if (min > arr[j]) {
min = arr[j];
k = j;
}
}
if(k != i) {
arr[k] = arr[i];
arr[i] = min;
}
//print(arr);
}
}
public static void print(int[] arr) {
for (int i = 0; i < arr.length; ++i) {
System.out.printf("%d ", arr[i]);
}
System.out.println();
}
}
插入排序
基本介绍
插入式排序属于内部排序法,是对欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。
插入排序思想
插入排序(Insertion Sorting)的基本思想是:
- 把n各待排序的元素看成一个有序表和一个无序表。
- 开始时,有序表中只有一个元素,无需表中有n-1个元素。
- 排序过程中,每次从无需表中取一个元素,把它放在排序码依次与有序表的排序码进行比较,将它插入到有序表的适当位置,使之成为新的有序表。
代码实现
import java.util.Scanner;
public class InsertionSort {
public static void main(String[] args) {
int n;
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; ++i) {
arr[i] = scanner.nextInt();
}
insertSort(arr);
print(arr);
}
public static void insertSort(int[] arr) {
int tmp;
for(int i = 1; i < arr.length; ++i) {// 外层循环为无序列表
for (int j = i-1; j>=0; j--) { // 内层循环为有序列表
if (arr[j+1] < arr[j]) {
tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
} else {
break;
}
}
}
}
public static void insertSort2(int[] arr) {
for(int i = 1; i < arr.length; ++i) {// 外层循环为无序列表
int insertValue = arr[i];
int insertIndex = i - 1; // 有序列表的最后一个索引
while(insertIndex>=0 && insertValue < arr[insertIndex]) {
arr[insertIndex+1] = arr[insertIndex];
insertIndex --;
}
arr[insertIndex +1] = insertValue;
// print(arr);
}
}
public static void print(int[] arr) {
for (int i = 0; i < arr.length; ++i) {
System.out.printf("%d ", arr[i]);
}
System.out.println();
}
}
希尔排序
基本介绍
希尔排序时希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序。它是简单插入排序经过改进后的一个更高效的版本,也成为缩小增量排序
。
基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法进行排序;随着增量逐渐减少,每组包含的关键词越来越多。当增量减至1时,整个文件恰好被分成一组,算法便终止。
代码实现
import java.util.Scanner;
public class ShellSort {
public static void main(String[] args) {
int n;
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; ++i) {
arr[i] = (int)(Math.random() * 800000);
}
long start = System.currentTimeMillis();
print(arr);
shellSort(arr);
System.out.println(System.currentTimeMillis() - start);
print(arr);
}
// 希尔排序 交换法
public static void shellSort(int[] arr) {
int gap = arr.length/2;
while (gap > 0) {
for (int i = gap; i < arr.length; ++i) {
for (int j = i - gap; j >= 0; j-=gap) {
if (arr[j] > arr[j + gap]) {
int temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
gap = gap/2;
//print(arr);
}
}
// 希尔排序 移动法
public static void shellSort2(int[] arr) {
int gap = arr.length / 2;
while (gap > 0) {
for (int i = gap; i < arr.length; ++i) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - gap]) {
while (j - gap >= 0 && temp < arr[j - gap]) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
gap /= 2;
}
}
public static void print(int[] arr) {
for (int i = 0; i < arr.length; ++i) {
System.out.printf("%d ", arr[i]);
}
System.out.println();
}
}
快速排序
基本介绍
快速排序(QuickSort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割称独立的两部分。其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
代码实现
import java.util.Scanner;
public class QuickSort {
public static void main(String[] args) {
int n;
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; ++i) {
arr[i] = (int)(Math.random()*800);
}
print(arr);
quickSort(arr, 0, arr.length-1);
print(arr);
}
public static void quickSort(int[] arr, int left, int right) {
if (left < right) {
int key = arr[left];
int l = left;
int r = right;
while (l < r) {
while (l < r && arr[r] > key) {
r--;
}
if (l < r) {
arr[l] = arr[r];
l++;
}
while (l < r && arr[l] < key) {
l++;
}
if (l < r) {
arr[r] = arr[l];
r--;
}
}
arr[l] = key;
quickSort(arr, left, l -1);
quickSort(arr, l+1, right);
}
}
public static void print(int[] arr) {
for (int i = 0; i < arr.length; ++i) {
System.out.printf("%d ", arr[i]);
}
System.out.println();
}
}
归并排序
基本介绍
归并排序(MergeSort)是利用归并的思想实现的排序方法。该算法采用经典的分治策略(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的答案“修补”在一起,即分而治之)。
代码实现
import java.util.Scanner;
public class MergeSort {
public static void main(String[] args) {
int n;
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; ++i) {
arr[i] = (int)(Math.random()*800);
}
print(arr);
int[] temp = new int[n];
mergeSort(arr, 0, arr.length-1, temp);
print(arr);
}
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right)/2;
mergeSort(arr, left, mid, temp);
mergeSort(arr, mid +1, right, temp);
merge(arr, left, mid, right, temp);
// System.out.println(left + " " + mid + " " + right);
// print(arr);
}
}
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int l = left;
int r = mid+1;
int t = 0;
while (l <= mid && r <= right) {
if (arr[l] <= arr[r]) {
temp[t++] = arr[l++];
} else {
temp[t++] = arr[r++];
}
}
while (l <= mid) {
temp[t++] = arr[l++];
}
while (r <= right) {
temp[t++] = arr[r++];
}
t = 0;
while (left <= right) {
arr[left++] = temp[t++];
}
}
public static void print(int[] arr) {
for (int i = 0; i < arr.length; ++i) {
System.out.printf("%d ", arr[i]);
}
System.out.println();
}
}
基数排序
基本介绍
- 基数排序属于“分配式排序”,又称为“桶子法”或 bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用。
- 基数排序法是属于稳定性的排序,基数排序法是高效且稳定的排序算法。
- 基数排序是桶排序的扩展。
- 基数排序是赫尔曼·何乐礼在1887年发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。
基本思想
将所有待比较的数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成后,数列就变成了一个有序序列。
代码实现
import java.util.Scanner;
public class RadixSort {
public static void main(String[] args) {
int n;
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; ++i) {
arr[i] = (int)(Math.random()*80000);
}
print(arr);
// long start = System.currentTimeMillis();
radixSort(arr);
// System.out.println(System.currentTimeMillis() - start);
print(arr);
}
public static void radixSort(int[] arr) {
int max = 0;
for (int i = 0; i < arr.length; ++i) {
if (max < Math.abs(arr[i])) {
max = Math.abs(arr[i]);
}
}
int length = 0;
do {
length ++;
max /= 10;
}while (max > 0);
// 定义一个二位数据
int[][] bucket = new int[10][arr.length];
int[] bucketCounts = new int[10];
// 根据尾数,将数据放入相应的桶中
for (int i = 0, n = 1; i < length; ++i, n*=10) {
for (int j = 0; j < arr.length; j++) {
int digtal = arr[j] / n %10;
bucket[digtal][bucketCounts[digtal]++] = arr[j];
}
// 将数据从桶中取出,放入原数组中
int index = 0;
for (int j = 0; j < bucketCounts.length; j++) {
if (bucketCounts[j] != 0) {
for (int l = 0; l < bucketCounts[j]; l++) {
arr[index++] = bucket[j][l];
}
}
bucketCounts[j] = 0; // 将桶中记录的元素个数置为0
}
}
}
public static void print(int[] arr) {
for (int i = 0; i < arr.length; ++i) {
System.out.printf("%d ", arr[i]);
}
System.out.println();
}
}
基数排序说明
- 基数排序是对传统桶排序的扩展,速度快。
- 基数排序是经典的空间换时间的方式,占用内存很大,当对海量数据排序时,容易造成OutOfMemoryError
- 基数排序时稳定的排序算法。
堆排序
基本介绍
- 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏、最好、平均时间复杂度都是O(nlogn),它是不稳定的排序算法。
- 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆。注意:没有要求结点的左孩子的值和右孩子的值的大小关系。
- 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
基本思想
- 将待排序的序列构造成一个大顶堆。
- 此时,整个序列的最大值就是堆顶的根结点。
- 将其与末尾元素进行交互,此时末尾的就为最大值。
- 让后将剩余n-1个元素重新构造成一个堆,这样就会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
代码实现
import java.util.Scanner;
public class HeapSort {
public static void main(String[] args) {
int n;
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; ++i) {
arr[i] = scanner.nextInt();
}
heapSort(arr);
print(arr);
}
public static void heapSort(int[] arr) {
for (int i = arr.length/2 - 1; i >= 0; i--) { // 调整非叶子节点
adjustHeap(arr, i, arr.length);
}
for (int j = arr.length-1; j > 0; j--) {
swap(arr, 0, j); // 堆顶元素,和当前元素进行交换
adjustHeap(arr, 0, j);
}
}
public static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[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 ++; // k指向的是左右子节点中,值较大的那个节点
}
if (arr[k] > temp) { // 如果子节点大于父节点,将子节点值赋给父节点
arr[i] = arr[k];
i = k;
} else {
break;
}
}
arr[i] = temp; // 将temp值,放到最终位置
}
public static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
public static void print(int[] arr) {
for (int i = 0; i < arr.length; ++i) {
System.out.printf("%d ", arr[i]);
}
System.out.println();
}
}