概念
排序
排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
平时的上下文中,如果提到排序,通常指的是排升序(非降序)。
通常意义上的排序,都是指的原地排序(in place sort) 。
稳定性(重要)
两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,则我们称该算法是具备稳定性的排序算法。
一个稳定的排序,可以实现为不稳定的排序
但是一个本身就不稳定的排序,是不可以变成稳定的排序
整个区间被分为
有序区间 [0, i),无序区间 [i, array.length)
每次选择无序区间的第一个元素,在有序区间内选择合适的位置插入
import java.util.Arrays;
public class TestDemo {
public static void insertSort(int[] array) {
for (int i = 1; i < array.length; i++) { // 假定第一个有序 从第二个开始
int tmp = array[i];
int j = i - 1;
for (; j >= 0; j--) {
if(array[j] > tmp) {
// if(array[j] >= tmp) { 加等号 不稳定
array[j + 1] = array[j];
} else {
// array[j + 1] = tmp; 只要j回退的时候,遇到了 比tmp小的元素就结束这次的比较
break;
}
}
//j回退到了 小于0 的地方
array[j + 1] = tmp;
}
}
public static void main(String[] args) {
int[] array = {3, 12, 5, 17, 2, 8};
insertSort(array);
System.out.println(Arrays.toString(array)); // [2, 3, 5, 8, 12, 17]
System.out.println(123);
}
}
直接插入排序:数据越有序,越快
经常用在,数据量不多 ,且趋近于有序
时间复杂度 | 空间复杂度 | ||
---|---|---|---|
最好 | 平均 | 最坏 | |
O(n) | O(n^2) | O(n^2) | O(1) |
数据有序 | 数据逆序 |
在有序区间选择数据应该插入的位置时,因为区间的有序性,可以利用折半查找的思想
public static void bsInsertSort(int[] array) {
for (int i = 1; i < array.length; i++) {
int v = array[i];
int left = 0;
int right = i;
// [left, right)
// 需要考虑稳定性
while (left < right) {
int m = (left + right) / 2;
if (v >= array[m]) {
left = m + 1;
} else {
right = m;
}
}
// 搬移
for (int j = i; j > left; j--) {
array[j] = array[j - 1];
}
array[left] = v;
}
}
思考: 假设现在有10000个数据如果对这组数据进行排序,使用插入排序:
10000个数据* 10000 = 1 0000 0000 -》1个亿
如果分成100组,每组使用插入排序:
100组100 100 =100 0000
100组100100=100 0000
如果采用分组的思想,我们会发现时间复杂度会有一个很大的改变。也就是希尔排序:
希尔排序法 又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。 然后,取重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
public class TestDemo {
/**
* @param array 待排序的序列
* @param gap 组数
*/
public static void sell(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {
int tmp = array[i];
int j = i - gap;
for (; j >= 0; j -= gap) {
if(array[j] > tmp) {
array[j + gap] = array[j];
} else {
break;
}
}
array[j + gap] = tmp;
}
}
/**
* 希尔排序
* @param array
*/
public static void sellSort(int[] array) {
int gap = array.length;
while(gap > 1) {
sell(array, gap);
gap /= 2;
}
sell(array, 1); // 保证最后是 1 组
}
}
时间复杂度:和增量有关
时间复杂度 | 空间复杂度 |
---|---|
O(n^1.3 到n^1.5) | O(1) |
稳定性:不稳定 | |
看在比较的过程当中是否发生了跳跃式的交换如果发生了跳跃式的交换 | |
那么就是不稳定的排序 |
j
每次选出最大(或最小)的一个元素,存放在无序区间的最后(或最前),i
有序,然后i
++,直到全部待排序的数据元素排完
优化:每次获取最小值下标,用minIndex
保存,直接交换
public class TestDemo {
/**
* 选择排序
* @param array 待排序的序列
*/
public static void selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
int minIndex = i; // 最小值下标
for (int j = i + 1; j < array.length; j++) {
if(array[j] < array[minIndex]) {
minIndex = j;
}
}
int tmp = array[i];
array[i] = array[minIndex];
array[minIndex] = tmp;
}
}
public static void selectSort1(int[] array) {
for (int i = 0; i < array.length; i++) {
for (int j = i + 1; j < array.length; j++) {
if(array[j] < array[i]) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
}
}
}
时间复杂度 | 空间复杂度 |
---|---|
O(n^2) | O(1) |
数据不敏感-与数据有序无序无关 | 数据不敏感 |
稳定性:不稳定
int[] a = { 9, 2, 5a, 7, 4, 3, 6, 5b };
// 交换中该情况无法识别,保证 5a 还在 5b 前边
测试:
直接插入排序有序的情况是O(n) 原因是遇到小的数据直接break了。j 并不会再减减。
选择排序来说,有序无序都是O(N^2) 原因就是时间复杂度 != 运行时间。现在之所以有序的情况下,选择排序的时间更快,是因为,交换的少了。j 本质上还是在++;
public class TestDemo {
public static void test1(int capacity) {
int[] array = new int[capacity];
for (int i = 0; i < array.length; i++) {
array[i] = i;
}
long start = System.currentTimeMillis(); // 开始时间
//insertSort(array); // 2
//sellSort(array); // 4
selectSort(array); // 717
long end = System.currentTimeMillis(); // 结束时间
System.out.println(end - start);
}
public static void test2(int capacity) {
int[] array = new int[capacity];
Random random = new Random();
for (int i = 0; i < array.length; i++) {
array[i] = random.nextInt(capacity);
}
long start = System.currentTimeMillis(); // 开始时间
// insertSort(array); // 1806
//sellSort(array); // 12
selectSort(array); // 2362
long end = System.currentTimeMillis(); // 结束时间
System.out.println(end - start);
}
public static void main(String[] args) {
test1(10_0000);
test2(10_0000);
}
}
每一次从无序区间选出最小 + 最大的元素,存放在无序区间的最前和最后,直到全部待排序的数据元素排完
public class Test {
public static void selectSortOP(int[] array) {
int low = 0;
int high = array.length - 1;
// [low, high] 表示整个无序区间
// 无序区间内只有一个数也可以停止排序了
while (low <= high) {
int min = low;
int max = low;
for (int i = low + 1; i <= max; i++) {
if (array[i] < array[min]) {
min = i;
}
if (array[i] > array[max]) {
max = i;
}
}
swap(array, min, low);
if (max == low) {
max = min;
}
swap(array, max, high);
}
}
private static void swap(int[] array, int i, int j) {
int t = array[i];
array[i] = array[j];
array[j] = t;
}
}
优先级队列(堆)、java 对象的比较
基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数。
注意: 排升序要建大堆;排降序要建小堆
public class TestDemo {
/**
* 堆排序
* @param array
*/
public static void headSort(int[] array) {
// 1、建堆 O(N)
creatHeap(array);
int end = array.length - 1;
while (end > 0) {
// 2、交换调整 O(NlogN)
swap(array, 0, end);
shiftDown(array, 0, end);
end--;
}
}
public static void creatHeap(int[] array) {
for (int parent = (array.length-1-1)/2; parent >= 0; parent--) {
shiftDown(array, parent, array.length);
}
}
public static void shiftDown(int[] array, int parent, int len) {
int child = 2 * parent + 1; // 左孩子下标
while(child < len) {
if(child + 1 < len && array[child] < array[child + 1]) {
child++; // 指向最大的孩子下标
}
if(array[child] > array[parent]) {
swap(array, child, parent);
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
稳定性:不稳定
时间复杂度 | 空间复杂度 |
---|---|
O(N*logN) | O(1) |
数据不敏感 | 数据不敏感 |
在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序
public class TestDemo {
/**
* 冒泡排序
* @param array
*/
public static void bubbleSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - 1 - i; j++) {
if(array[j] > array[j + 1]) {
int tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
}
}
}
}
public static void bubbleSort1(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
boolean flg = true; // 假设有序
for (int j = 0; j < array.length - 1 - i; j++) {
if(array[j] > array[j + 1]) {
int tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
flg = false;
}
}
if(flg) {
break;
}
}
}
}
稳定性:稳定的
时间复杂度 | 空间复杂度 | |
---|---|---|
(优化的) | 未优化都是 | |
O(n) O(n^2) | O(n^2) | O(1) |
private static int partition(int[] array, int left, int right) {
int i = left;
int j = right;
int pivot = array[left];
while (i < j) {
while (i < j && array[j] >= pivot) {
j--;
}
while (i < j && array[i] <= pivot) {
i++;
}
swap(array, i, j);
}
swap(array, i, left);
return i;
}
private static int partition(int[] array, int left, int right) {
int d = left + 1;
int pivot = array[left];
for (int i = left + 1; i <= right; i++) {
if (array[i] < pivot) {
swap(array, i, d);
d++;
}
}
swap(array, d, left);
return d;
}
挖坑法基本思路和Hoare 法一致,只是不再进行交换,而是进行赋值(填坑+挖坑)
public class TestDemo {
/**
* 快速排序
* @param array
*/
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
// 递归基准左右区间
public static void quick(int[] array, int left, int right) {
if(left >= right) {
return;
}
int pivot = partition(array, left, right); // 基准
quick(array, left, pivot - 1);
quick(array, pivot + 1, right);
}
// 找基准
public static int partition(int[] array, int start, int end) {
int tmp = array[start];
while(start < end) {
while(start < end && array[end] >= tmp) { // 不加等号将死循环
end--; // 从后往前找一个 < tmp 的值
}
array[start] = array[end];
while(start < end && array[start] <= tmp) {
start++; // 从前往后找一个 > tmp 的值
}
array[end] = array[start];
}
array[start] = tmp; // start 和 end 相遇的位置就是基准
return start;
}
}
问题:
测试一百万数据 -> 栈溢出了
所以我们需要优化
稳定性:不稳定
时间复杂度 | 空间复杂度 | ||
---|---|---|---|
最好(每次可以均匀地分割待排序 序列) | 最坏(数据有序 或逆序) | 最好 | 最坏(单分支的一棵树) |
O(K*N*logn) | O(n^2) | O(logn) | O(n) |
public class TestDemo {
/**
* 快速排序
* @param array
*/
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
// 递归基准左右区间
public static void quick(int[] array, int left, int right) {
if(left >= right) {
return;
}
// 2、如果区间内的数据 在排序的过程中 小于某个阈值 使用直接插入排序
if(right - left + 1 <= 1500) {
insertSort2(array, left, right);
return;
}
// 1、三数取中法 找基准之前,我们找到中间大小的值 交换
int midValIndex = findMidValIndex(array, left, right);
swap(array, midValIndex, left);
int pivot = partition(array, left, right); // 基准
quick(array, left, pivot - 1);
quick(array, pivot + 1, right);
}
// (2.) 直接插入排序
public static void insertSort2(int[] array, int start, int end) {
for (int i = 1; i < end; i++) { // 假定第一个有序 从第二个开始
int tmp = array[i];
int j = i - 1;
for (; j >= start; j--) {
if(array[j] > tmp) {
// if(array[j] >= tmp) { 加等号 不稳定
array[j + 1] = array[j];
} else {
// array[j + 1] = tmp; 只要j回退的时候,遇到了 比tmp小的元素就结束这次的比较
break;
}
}
//j回退到了 小于0 的地方
array[j + 1] = tmp;
}
}
// (1.) 找三数中间值下标
public static int findMidValIndex(int[] array, int start, int end) {
int mid = start + ((end - start) >>> 1);
if(array[start] < array[end]) {
if(array[mid] < array[start]) {
return start;
} else if (array[mid] > array[end]) {
return end;
} else {
return mid;
}
} else {
if(array[mid] > array[start]) {
return start;
} else if (array[mid] < array[end]) {
return end;
} else {
return mid;
}
}
}
// 找基准
public static int partition(int[] array, int start, int end) {
int tmp = array[start];
while(start < end) {
while(start < end && array[end] >= tmp) {
end--; // 从后往前找一个 < tmp 的值
}
array[start] = array[end];
while(start < end && array[start] <= tmp) {
start++; // 从前往后找一个 > tmp 的值
}
array[end] = array[start];
}
array[start] = tmp; // start 和 end 相遇的位置就是基准
return start;
}
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
先找一次基准,将left
基准减一 基准加一 right
一次放入栈中
拿出的时候,根据放入的顺序,先拿出的是 right
前提:
Pivo 左边有2个元素:pivot > left+1
右边有2个元素:pivot < right-1
代码:
public class TestDemo {
/**
* 非递归 快速排序
* @param array
*/
public static void quickSort(int[] array) {
Stack<Integer> stack = new Stack<>();
int left = 0;
int right = array.length - 1;
int pivot = partition(array, left, right);
if(pivot > left + 1) {
// 左边有两个元素
stack.push(left);
stack.push(pivot - 1);
}
if(pivot < right - 1) {
// 右边有两个元素
stack.push(pivot + 1);
stack.push(right);
}
while(!stack.isEmpty()) {
right = stack.pop(); // 先右
left = stack.pop();
pivot = partition(array, left, right);
if(pivot > left + 1) {
stack.push(left);
stack.push(pivot - 1);
}
if(pivot < right - 1) {
stack.push(pivot + 1);
stack.push(right);
}
}
}
// 找基准
public static int partition(int[] array, int start, int end) {
int tmp = array[start];
while(start < end) {
while(start < end && array[end] >= tmp) {
end--; // 从后往前找一个 < tmp 的值
}
array[start] = array[end];
while(start < end && array[start] <= tmp) {
start++; // 从前往后找一个 > tmp 的值
}
array[end] = array[start];
}
array[start] = tmp; // start 和 end 相遇的位置就是基准
return start;
}
}
归并排序(MERGE-SORT) 是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
分解与合并:
两有序数组合并:
要学习归并排序,我们要知道如何合并两个有序数组,
思路:array1 记s1
遍历,array2 记s2
,遍历比较哪个数组元素小,就把哪个数组元素放到新数组中(新数组大小是两数组长度之和),用i
记录下一个待放的位置,此数组往后走,在比较两数组,一个数组走完,将另一个数组剩下的元素放到新数组中,
两有序数组合并代码如下:
public class TestDemo {
/**
* 有序数组合并
* @param array1 有序
* @param array2 有序
* @return
*/
public static int[] mergeArray(int[] array1, int[] array2) {
int[] merge = new int[array1.length + array2.length];
int i = 0;
int s1 = 0;
int s2 = 0;
while (s1 < array1.length && s2 < array2.length) {
if (array1[s1] <= array2[s2]) {
merge[i++] = array1[s1++];
} else {
merge[i++] = array2[s2++];
}
}
while (s1 < array1.length) {
merge[i++] = array1[s1++];
}
while (s2 < array2.length) {
merge[i++] = array2[s2++];
}
return merge;
}
}
public class TestDemo {
/**
* 归并排序
* @param array
*/
public static void mergeSort(int[] array) {
mergeSortInternal(array, 0, array.length - 1);
}
// 分解
public static void mergeSortInternal(int[] array, int low, int high) {
if(low >= high) { // 相遇分解完成
return;
}
int mid = low + ((high - low) >>> 1);
// 分解左右
mergeSortInternal(array, low, mid);
mergeSortInternal(array, mid + 1, high);
// 合并
merge(array, low, mid, high);
}
// 合并
private static void merge(int[] array, int low, int mid, int high) {
int[] tmp = new int[high - low + 1];
int k = 0;
int s1 = low;
int e1 = mid;
int s2 = mid + 1;
int e2 = high;
while (s1 <= e1 && s2 <= e2) {
if(array[s1] <= array[s2]) { // 去等号就是不稳定
tmp[k++] = array[s1++];
}else {
tmp[k++] = array[s2++];
}
}
while (s1 <= e1) {
tmp[k++] = array[s1++];
}
while (s2 <= e2) {
tmp[k++] = array[s2++];
}
// 拷贝tmp数组的元素 放入原来的数组array当中
for (int i = 0; i < k; i++) {
array[i + low] = tmp[i]; // i + low 可以对应后半段位置
}
}
}
稳定性:稳定的
时间复杂度 | 空间复杂度 |
---|---|
O(N*logN) | O(n) |
数据不敏感 | 数据不敏感 |
目前所学的排序,只有三个是稳定的:
冒泡排序,插入排序,归并排序
代码如下:
public class TestDemo {
/**
* 非递归 归并排序
* @param array
*/
public static void mergeSort(int[] array) {
int nums = 1; // 每组的数据个数 从每组1个开始
while(nums < array.length) { // 组数直到len-1停止
// 数组每次都要进行遍历
for (int i = 0; i < array.length; i += nums * 2) {
int left = i;
int mid = left + nums - 1;
if(mid > array.length) { // mid 可能超出范围
mid = array.length - 1;
}
int right = mid + nums;
if(right > array.length) { // right 也有可能超出范围
right = array.length - 1;
}
// 确定下标后 合并
merge(array, left, mid, right);
}
nums *= 2;
}
}
// 合并
private static void merge(int[] array, int low, int mid, int high) {
int[] tmp = new int[high - low + 1];
int k = 0;
int s1 = low;
int e1 = mid;
int s2 = mid + 1;
int e2 = high;
while (s1 <= e1 && s2 <= e2) {
if(array[s1] <= array[s2]) { // 去等号就是不稳定
tmp[k++] = array[s1++];
}else {
tmp[k++] = array[s2++];
}
}
while (s1 <= e1) {
tmp[k++] = array[s1++];
}
while (s2 <= e2) {
tmp[k++] = array[s2++];
}
// 拷贝tmp数组的元素 放入原来的数组array当中
for (int i = 0; i < k; i++) {
array[i + low] = tmp[i]; // i + low 可以对应后半段位置
}
}
}
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
1.10 基数排序 | 菜鸟教程
10进制的数从小到大排序,取十个桶,
以个位数放入,依次取出,此时个位数已有序
在以十位数排序,以此类推,次数为最大数的位数
桶排序 (Bucket sort) 或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间O(n)。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。
排序方法: 申请一个计数数组count[]
,遍历待排序的数组,对应的下标加一,
遍历完成后,遍历count[]
,依次获取每一个下标元素不为0
的,
如 0下标为2,就是 0 0 ,3下标为1,就是3,遍历完成就是排好序的数组了
适合用: 有n个数,取值范围是 0~n,写出一个排序算法,要求时间复杂度和空间复杂度都是O(n)的
*问题:
如何确定计数数组大小?
这个计数排序,适合用在0-99
范围
解决方法:
1、求数组的最大值maxVal
,最小是minVal
maxVal - minVal + 1 = 数组的长度
2、count [array[i]-minVal]
public class TestDemo {
/**
* 计数排序
* @param array
*/
public static void countingSort(int[] array) {
// 找数组最大值和最小值
int maxVal = array[0];
int minVal = array[0];
for (int i = 1; i < array.length; i++) {
if(array[i] > maxVal) {
maxVal = array[i];
}
if(array[i] < minVal) {
minVal = array[i];
}
}
// 统计数组每个数据出现的次数
int[] count = new int[maxVal - minVal + 1];
for (int i = 0; i < array.length; i++) {
// 为了空间的合理使用 这里需要index-minVal 防止923-90
count[array[i] - minVal]++;
}
// 因为默认是0 所以不为0的拿出来 把数据写回数组
int indexArray = 0;
for (int i = 0; i < count.length; i++) {
while(count[i] != 0) {
// 注意 对应回去要加 minVal
array[indexArray++] = i + minVal; // 注意 下标要向后移动
count[i]--; // 拷贝一个 次数就少一个
}
}
}
}
稳定性:当前代码不稳定,但本质是稳定的
时间复杂度 | 空间复杂度 |
---|---|
O(n) | O(m) m: 和数组范围有关 |