概述
排序的需求 ,例如建立了一个很重要的数据库之后可能根据某些需求对数据进行不同的排序。如姓名按字母排序,学生按成绩排序等等。
如何排序?
假设现在有一排学生要求按身高排序,如果是人工来排序,我们可以看见所有的学生,通过目测可以轻松的比较学生的身高,并且不需要额外的空间,学生推推搡搡的就腾出了位置,然后交换,很简单就排好顺序。但是计算机却不能像人一样通览数据,只能在同一时间内对两人进行比较,遵循一些简单规则一步步解决问题。并且可能需要额外空间。
排序
排序基本分类:
冒泡排序
- 复杂度
时间复杂度:最差、平均都是O(n^2),最好是O(n)
空间复杂度:1
- 执行顺序
从第0个元素开始,与后边的一个元素比较,如果比后边的元素大,则交换位置,此时第1个元素继续与第2个元素比较,以此类推,比第1个元素小则不动。
代码实现
public static int[] bubble_sort2(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length-1-i; j++) {
if (arr[j+1] < arr[j]) {
swap(arr, j+1, j);
}
}
}
return arr;
}
这里也可能出现最后几次没有比较的情况,所以可以做一些优化改进。
比如数组元素如下:
int[] arr = { 1, 2, 3, 6, 5, 17, 9, 8, 12, 4 };
每次交换之后的顺序为:
1 2 3 5 6 9 8 12 4 17
1 2 3 5 6 8 9 4 12 17
1 2 3 5 6 8 4 9 12 17
1 2 3 5 6 4 8 9 12 17
1 2 3 5 4 6 8 9 12 17
1 2 3 4 5 6 8 9 12 17
1 2 3 4 5 6 8 9 12 17
1 2 3 4 5 6 8 9 12 17
1 2 3 4 5 6 8 9 12 17
1 2 3 4 5 6 8 9 12 17
可见最后几次是没有必要的,这是因为前边的几个元素的顺序已经符合要求了,最后几次没必要交换。所以针对冒泡排序可以做一些改进:
public static int[] bubble_sort2(int[] arr) {
boolean isChanged = true;
for (int i = 0; i < arr.length && isChanged; i++) {
isChanged = false;
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j + 1] < arr[j]) {
swap(arr, j + 1, j);
isChanged = true;
}
}
}
return arr;
}
这里用了一个boolean型的标记位,如果某一次循环没有改变任何位置,说明已经排好了,不需要继续进行。
优化之后:
1 2 3 5 6 9 8 12 4 17
1 2 3 5 6 8 9 4 12 17
1 2 3 5 6 8 4 9 12 17
1 2 3 5 6 4 8 9 12 17
1 2 3 5 4 6 8 9 12 17
1 2 3 4 5 6 8 9 12 17 (产生交换)
1 2 3 4 5 6 8 9 12 17 (检测到本次没有任何交换,不再继续)
优化之后可以看到只进行了7次循环,比原来10次减少3次。
快速排序
原理:
快速排序是对冒泡排序的一种改进,通过一趟排序将待排序的记录分割成两部分,其中一部分的关键字均比另一部分的关键字小,则可以对两部分继续进行排序,以达到整个序列有序。进行一次排序 处理顺序:
然后在分别对左右两边使用同样的方法进行递归
代码实现
public static int[] quick_sort(int[] arr) {
qsort(arr, 0, arr.length - 1);
return arr;
}
private static void qsort(int[] arr, int low, int high) {
int pivot;
if (low < high) {//递归处理
pivot = partition(arr, low, high);
qsort(arr, low, pivot);
qsort(arr, pivot + 1, high);
}
}
private static int partition(int[] arr, int low, int high) {//根据关键字拆分成两列
int pivotkey;
pivotkey = arr[low];// 选择pivot,此处可以优化
while (low < high) {
while (low < high && arr[high] >= pivotkey) {
high--;
}
swap(arr, low, high);// 交换,此处可以优化
while (low < high && arr[low] <= pivotkey) {
low++;
}
swap(arr, low, high);
}
return low;
}
private static void swap(int[] arr, int one, int two) {
int temp = arr[one];
arr[one] = arr[two];
arr[two] = temp;
}
选择pivot是效率的至关重要的因素,理想情况下应该为中位数。
快速排序适合大量数据,代码稍复杂,需要熟悉,也是面试常考内容。
简单插入排序
思想:
给定序列,存在一个分界线,分界线的左边被认为是有序的,分界线的右边还没被排序,每次取没被排序的最左边一个和已排序的做比较,并插入到正确位置;我们默认索引0的子数组有序;每次循环将分界线右边的一个元素插入有序数组中,并将分界线向右移一位;
代码实现:
public static int[] insertion_sort(int[] arr) {
int j;
for (int i = 1; i < arr.length; i++) {
if (arr[i] < arr[i - 1]) {
int tmp = arr[i];
for (j = i - 1; j >= 0 && arr[j] > tmp; j--) {
arr[j + 1] = arr[j];
}
arr[j + 1] = tmp;
}
}
return arr;
}
希尔排序
思想:由于简单插入排序对于记录较少或基本有序时很有效,因此我们可以通过将序列进行分组排序使得每组容量变小,再进行分组排序,然后进行一次简单插入排序即可;
这里的分组是跳跃分组,即第1,4,7位置为一组,第2,5,8位置为一组,第3,6,9位置为一组;
这里的分组是跳跃分组,比如分3组。
1 2 3 4 5 6 7 8 那么分组情况就是 1 4 7,2 5 8,3 6
此时,如果increment=3,则i%3相等的索引为一组,比如索引1,1+3,1+3*2
一般增量公式为:increment = increment/3+1;
代码实现:
public static int[] shell_sort(int[] arr) {
int j;
int increment = arr.length;
do {
increment = increment / 3 + 1;
for (int i = increment; i < arr.length; i++) { //i=increment 因为插入排序默认每组的第一个记录都是已排序的
if (arr[i] < arr[i - increment]) {
int tmp = arr[i];
for (j = i - increment; j >= 0 && arr[j] > tmp; j -= increment) {
arr[j + increment] = arr[j];
}
arr[j + increment] = tmp;
}
}
} while (increment > 1);
return arr;
}
直接选择排序
简单选择排序特点:每次循环找到最小值,并交换,因此交换次数始终为n-1次;
相对于最简单的排序,对于很多不必要的交换做了改进,每个循环不断比较后记录最小值,只做了一次交换(当然也可能不交换,当最小值已经在正确位置)
//最差:n(n-1)/2次比较,n-1次交换,因此时间复杂度为O(n^2)
//最好:n(n-1)/2次比较,不交换,因此时间复杂度为O(n^2)
//好于冒泡排序
public static int[] selection_sort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[min] > arr[j]) {
min = j;
}
}
if (min != i)
swap(arr, min, i);
}
return arr;
}
堆排序
首先明确两个概念:
大根堆:任意父节点都比子节点大;
小根堆:任意父节点都比子节点小;思想:构建一棵完全二叉树,首先构建大根堆,然后每次都把根节点即最大值移除,并用编号最后的节点替代,这时数组长度减一,然后重新构建大根堆,以此类推;
注意:此排序方法不适用于个数少的序列,因为初始构建堆需要时间;
public static int[] heap_sort(int[] arr) {
int tmp[] = new int[arr.length + 1];
tmp[0] = -1;
for (int i = 0; i < arr.length; i++) {
tmp[i + 1] = arr[i];
}
// 构建大根堆:O(n)
for (int i = arr.length / 2; i >= 1; i--) {
makeMaxRootHeap(tmp, i, arr.length);
}
// 重建:O(nlogn)
for (int i = arr.length; i > 1; i--) {
swap(tmp, 1, i);
makeMaxRootHeap(tmp, 1, i - 1);
}
for (int i = 1; i < tmp.length; i++) {
arr[i - 1] = tmp[i];
}
return arr;
}
private static void makeMaxRootHeap(int[] arr, int low, int high) {
int tmp = arr[low];
int j;
for (j = 2 * low; j <= high; j*=2) {
if (j < high && arr[j] < arr[j + 1]) {
j++;
}
if (tmp >= arr[j]) {
break;
}
arr[low] = arr[j];
low = j;
}
arr[low] = tmp;
}
归并排序
思想:利用递归进行分割和合并,分割直到长度为1为止,并在合并前保证两序列原本各自有序,合并后也有序;
public static int[] merge_sort(int[] arr) {
Msort(arr, arr, 0, arr.length - 1);
return arr;
}
private static void Msort(int[] sr, int[] tr, int s, int t) {
int tr2[] = new int[sr.length];
int m;
if (s == t) {
tr[s] = sr[s];
} else {
m = (s + t) / 2;
Msort(sr, tr2, s, m);
Msort(sr, tr2, m + 1, t);
Merge(tr2, tr, s, m, t);
}
}
private static void Merge(int[] tr2, int[] tr, int i, int m, int t) {
int j, k;
for (j = i, k = m + 1; i <= m && k <= t; j++) {
if (tr2[i] < tr2[k]) {
tr[j] = tr2[i++];
} else {
tr[j] = tr2[k++];
}
}
while (i <= m) {
tr[j++] = tr2[i++];
}
while (k <= t) {
tr[j++] = tr2[k++];
}
}
—–各算法时间复杂度和空间复杂度比较:
关于排序暂时就写这么多,每一种方法都有自己的特色和优缺点,要根据实际情况选择不同的排序才能有最大的效率,每一种都需要熟记于心。
本文部分参考了前辈们的观点,非常感谢。