1 排序: 就是按照其中某个或某些关键字的大小,递增或递减排列起来的操作.通常我们是排升序.
2 稳定性(重要): 两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,我们称该算法是具有稳定性的算法. 冒泡 插入 归并 是稳定的排序.
3 七大排序算法(咋们都按升序排)及其比较:
(1) 插入排序: 每次选择无序区间的第一个元素,在有序区间内选择合适的位置插入.
public static void insertSort(int[] array) {
// 通过 bound 来划分出两个区间, [0, bound)已排序区间, [bound, size)待排序区间
for (int bound = 1; bound < array.length; bound++) {
int v = array[bound];
int cur = bound - 1; // 已排序区间的最后一个元素下标
for (; cur >= 0; cur--) {
注意!!!! 这个条件如果写成 >= , 咱的插入排序就不是稳定排序了
if (array[cur] > v) {
array[cur + 1] = array[cur];
} else {
// 此时说明已经找到了合适的位置
break;
}
}
array[cur + 1] = v;
}
}
(2) 希尔排序(插入排序的优化): 先确定一个整数gap为所分组数, gap=数组长度/2.
从第一个元素开始, 所有距离为 gap 的元素被 两两分为一组进行比较. 然后让 gap=gap/2.
直到最后 gap=1 时相当于插入排序, 排序就完成了.
时间复杂度 O(N^1.3).
public static void shellSort(int[] array) {
int gap = array.length / 2;
while (gap > 1) {
// 需要循环进行分组插排
insertSortGap(array, gap);
gap = gap / 2;
}
insertSortGap(array, 1); -----> 当gap=1时, 相当于插入排序.
}
private static void insertSortGap(int[] array, int gap) {
// 通过 bound 来划分出两个区间, [0, bound)已排序区间, [bound, size)待排序区间
// 当把gap 替换成 1 的时候, 理论上这个代码就和前面的插入排序代码一模一样.
for (int bound = gap; bound < array.length; bound++) {
int v = array[bound];
int cur = bound - gap; // 这个操作是在找同组中的上一个元素
for (; cur >= 0; cur -= gap) {
// 注意!!!! 这个条件如果写成 >= , 咱的插入排序就不是稳定排序了
if (array[cur] > v) {
array[cur + gap] = array[cur];
} else {
// 此时说明已经找到了合适的位置
break;
}
}
array[cur + gap] = v;
}
}
(3) 选择排序: 以bound(待排区间的首元素) 元素作为擂主, 循环从待排序区间bound的下一个元素开始, 依次取出元素和擂主进行比较.
在一趟排序中, 如果打擂成功, 该元素就成为擂主元素. 然后下一个元素继续和新的擂主元素比较. 直到一趟排序到最后一个元素结束. 把最后更新的新的擂主元素和一开始的擂主元素进行交换.
一趟排序只交换一次.
public static void selectSort(int[] array) {
for (int bound = 0; bound < array.length-1; bound++) {
int i = bound;
for (int cur = bound + 1; cur < array.length; cur++) {
if (array[cur] < array[bound]) {
i = cur;
}
}
//交换i和bound
int tmp = array[i];
array[i] = array[bound];
array[bound] = tmp;
}
}
(4) 堆排序: (排升序建大堆, 降序建小堆) 先建好堆, 比如大堆. 先把堆顶元素和堆尾元素交换, 堆尾就是最大元素了. 然后让堆的 size - - , 对堆顶元素进行一次向下调整, 直到size =1( 类似于优先级队列的出队列).
public static void heapSort(int[] array) {
createHeap(array);
for (int i = 0; i < array.length - 1; i++) {
// 当前堆的元素个数
int heapSize = array.length - i;
// 交换 堆顶元素和堆的 最后一个元素
swap(array, 0, heapSize - i - 1);
heapSize--; //就把最后一个元素从堆中排除掉, 让堆的size--
// [0, array.length-i-1)就是待排序区间 [array.length-i-1, array.length)就是已排序区间
shiftDown(array, heapSize, 0); 为了不破坏堆的构造,交换后再按照向下调整建堆.
}
}
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
private static void createHeap(int[] array) {
for (int i = (array.length - 1 - 1) / 2; i >= 0; i--) {
shiftDown(array, array.length, i);
}
}
private static void shiftDown(int[] array, int heapLength, int index) {
int parent = index;
int child = 2 * parent + 1;
while (child < heapLength) {
if (child + 1 < heapLength && array[child + 1] > array[child]) {
child = child + 1;
}
if (array[child] > array[parent]) {
swap(array, child, parent);
} else {
break;
}
parent = child;
child = 2 * parent + 1;
}
}
(5) 冒泡排序:从无序区间开始, 从后往前 通过两两相邻元素之间的比较找到最小值, 放到待排区间最前面. 如果是从前往后 比较的话就把找到的最小值放到待排区间最后面.
每趟排序最多交换 i-j 次.
//注意避免下标越界异常出现
public static void bubbleSort(int[] array) {
// 按照每次找最小并放在最前面的方式来进行排序. (从后往前比较交换)
for (int bound = 0; bound < array.length; bound++) {
// [0, bound) 已排序区间 [bound, size) 待排序区间
// cur > bound 而不是 >= , 当 bound 为 0 的时候, 如果 >= , cur 也为 0, cur - 1 也就下标越界了
for (int cur = array.length - 1; cur > bound; cur--) {
// 此处 cur - 1 是因为 cur 初始值是 array.length - 1. 如果取 cur + 1 下标的元素, 就越界了
// 此处的条件如果写成 >= 同样无法保证稳定性
if (array[cur - 1] > array[cur]) {
swap(array, cur - 1, cur);
}
}
}
}
(6) 快速排序(重要) 原理: partition(隔离).
第一种交换方法 (先左右交换,最后再与基准值交换):
a 通常选最左边或最右边元素作为基准值 base.
b 如果取最左侧元素为基准值,就从右边开始. 先让right从最右边找一个小于基准值的元素直到找到,然后让left从最左边开始找一个大于基准值的直到找到,然后和刚才找到的右边的元素交换位置.
c 然后重复进行以上动作,直到left和right重合. 重合后把基准值和重合元素再交换.
d 以基准值为分割点就把元素分成两部分了. 基准值左边的元素都小于(可以包含相等的)基准值, 基准值右边的元素都大于(可以包含相等的)基准值. 对接下来的两部分元素再递归执行以上动作直到一个阈值.
e 基准值的选取很重要. 通常选取一个中位数的元素放到数组首或尾. 当递归到一定程度时再递归效率就不高了,可以把剩下的基本有序的元素采取 插入排序提高效率.
另一种交换方法 (每次找到后都与基准值交换): 如果取最左侧元素为基准值,就从右边开始找小于基准值的,找到后直接与基准值交换. 然后从左边找大于基准值的,找到后也直接与基准值交换.
还有其它的方法, 省略了.
public static void quickSort(int[] array) {
// 辅助完成递归过程, 此处为了代码简单, 区间设定成前闭后闭.
quickSortHelper(array, 0, array.length - 1);
}
private static void quickSortHelper(int[] array, int left, int right) {
if (left >= right) {
// 区间中有 0 个元素或者 1 个元素. 此时不需要排序,相当于阈值.
return;
}
int index = partition(array, left, right); index就是 新的基准值的位置.
quickSortHelper(array, left, index - 1); 递归基准值 左侧.
quickSortHelper(array, index + 1, right); 递归基准值 右侧.
private static int partition(int[] array, int left, int right) {
int beg = left;
int end = right;
// 取最右侧元素为基准值
int base = array[right];
while (beg < end) {
// 从左往右找到比基准值大的元素
while (beg < end && array[beg] <= base) {
beg++;
}
// 当上面的循环结束时, beg 要么和 end 重合, 要么 beg 就指向一个大于 base 的值
while (beg < end && array[end] >= base) {
end--;
}
// 当上面的循环结束之后, beg 要门和 end 重合,要么 end 就指向一个小于 base 的值
swap(array, beg, end);
}
beg 位置的元素一定 >= 基准值,然后最后一步让 beg位置的元素和 基准值再交换.
swap(array, beg, right);
return beg; ===> 注意返回的是 新的基准值的下标.
}
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
下列选项中,不可能是快速排序第2趟排序结果的是 ( C ).
A.2,3,5,4,6,7,9
B.2,7,5,6,4,3,9
C.3,2,5,4,7,6,9
D.4,2,3,5,7,6,9
快排的阶段性排序结果的特点是: 第 i 趟完成时,会有 i及i以上 的数出现在它最终将要出现的位置,即它左边的数都比它小, 它右边的数都比它大. 题目问第二趟排序的结果,即要找不存在2个这样的数的选项。A选项中2、3、6、7、9均符合,所以A排除;B选项中,2、9均符合,所以B排除;D选项中5、9均符合,所以D选项排除;最后看C选项,只有9一个数符合, 所以选C.
(7) 归并排序(重要)
a 适用于外部排序(数据在磁盘上), 也适用于链表排序.
b 过程: 先把一个区间分成两份, 然后把每个子区间继续分成两份, 直到每个区间都是一个元素. 当某个区间只有一个元素时,这个区间一定是有序的, 最后合并每两个相邻区间(同时进行排序),直到成为一个区间.
c 注意和希尔排序的区别, 那个是每一步都在排序, 这个是先把所有元素分成单个的一份, 然后两两进行排序. 然后把两两排序好的小组当成两个新的小组继续排序. 是一个 递归过程.
public static void mergeSort(int[] array) {
mergeSortHelper(array, 0, array.length);
}
private static void mergeSortHelper(int[] array, int low, int high) {
if (high - low <= 1) {
return;
}
int mid = (low + high) / 2;
// 这个方法执行完, 就认为 low, mid 已经排序ok
mergeSortHelper(array, low, mid);
// 这个方法执行完, 就认为, mid, high 也已经排序ok
mergeSortHelper(array, mid, high);
// 当把左右区间已经归并排序完了, 说明左右区间已经是有序区间了.
// 接下来就可以针对两个有序区间进行合并了.
merge(array, low, mid, high);
}
public static void merge(int[] array, int low, int mid, int high) {
int[] output = new int[high - low];
int outputIndex = 0; // 记录当前 output 数组中被放入多少个元素了
int cur1 = low;
int cur2 = mid;
while (cur1 < mid && cur2 < high) {
// 这里写成 <= 才能保证稳定性.
if (array[cur1] <= array[cur2]) {
output[outputIndex] = array[cur1];
outputIndex++;
cur1++;
} else {
output[outputIndex] = array[cur2];
outputIndex++;
cur2++;
}
}
// 当上面的循环结束的时候, 肯定是 cur1 或者 cur2 有一个先到达末尾, 另一个还剩下一些内容
// 把剩下的内容都一股脑拷贝到 output 中
while (cur1 < mid) {
output[outputIndex] = array[cur1];
outputIndex++;
cur1++;
}
while (cur2 < high) {
output[outputIndex] = array[cur2];
outputIndex++;
cur2++;
}
// 把output中的元素再搬运回原来的数组
for (int i = 0; i < high - low; i++) {
array[low + i] = output[i];
}
}
总结:
4 七大排序的 时间复杂度, 空间复杂度, 稳定性.
5 其他 非基于 比较 的排序: 计数排序 基数排序 桶排序.
计数排序 是 稳定排序. 当输入的元素是 n个 0到k 之间的整数时, 时间复杂度是 O(n+k), 空间复杂度也是 O(n+k).
其排序速度 快于任何比较排序算法. 适用于: 当k不是很大并且序列比较集中时.
6 TopK问题.
一: 对k个数进行排序的情况.
(1) 全局排序, O(n^2). 对所有数排序.
(2) 局部排序, 如冒泡排序, O(n*k). 只排序TopK.
(3) 分治法: 快速排序, O(n*lg(n)). 对所有数排序(每个分支都要递归).
(4) 使用堆: (优先级队列建堆), O(n * lg(k)). 只排序TopK.
当找 topk最大的数时, 先用前k个元素生成一个 小堆,这个小堆用于存储 当前最大的k个元素. 从第k+1个元素开始扫描,和堆顶(堆中最小的元素)比较,如果被扫描的元素大于堆顶,则替换堆顶的元素并调整堆, 以保证堆内的k个元素总是当前最大的k个元素.
找topk大的数, 使用 优先级队列建k个按 升序排的小堆.
找topk小的数, 使用 优先级队列建k个按 降序排的大堆.
例: 找topK小的数.
import java.util.ArrayList;
import java.util.Collections;
import java.util.PriorityQueue;
public class DemoB {
public ArrayList<Integer> topK(int[] input, int k) {
ArrayList<Integer> list = new ArrayList<>();
if (input == null || k <= 0 || k > input.length) {
return list;
}
我们使用优先级队列建堆.
优先级队列默认是升序排, 我们现在传入参数让它 降序排k个元素,是一个大堆.
PriorityQueue<Integer> queue=new PriorityQueue(k, Collections.reverseOrder());
for(int i=0;i<input.length;i++){
if(i<k){
queue.offer(input[i]); // queue 会自动排序.
}else {
if(input[i]<queue.peek()){
queue.poll();
queue.offer(input[i]); queue会自动更新排序.
}
}
}
for(int i=0;i<queue.size();i++){
list.add(queue.poll());
}
return list;
}
}
二: 找出topK, 这k个数没有排序的情况.
(5) 减治法: 快排+选择, O(n). (只进行一次partition, 只递归一个分支).
例: 找topK小的数.
public class TopK1 {
public static void findK(int[] a, int n, int K) {
findKth(a, 0, n - 1, K);
}
public static void findKth(int[] a, int left, int right, int k) {
int index = partition(a, left, right);
if (k == index - left + 1) {
for (int i = 0; i <= index; i++) {
System.out.print(a[i] + " ");
}
} else if (k > index - left + 1) {
findKth(a, index + 1, right, k - index + left - 1);
} else findKth(a, left, index - 1, k);
}
排升序.
public static int partition(int[] a, int left, int right) {
int beg = left;
int end = right;
int base = a[right];
while (beg < end) {
while (beg < end && a[beg] <= base) {
beg++;
}
while (beg < end && a[end] >= base) {
end--;
}
swap(a, beg, end);
}
swap(a, beg, right);
return beg;
}
private static void swap(int[] a, int i, int j) {
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
}
过程: 按照降序排序,然后只进行一次partition,找到中轴index. 然后判断 k 与 index - left + 1 的关系. (1) 如果 k==index - left + 1,说明中轴index即为第k大元素.
(2) 如果 k>index - left + 1,说明第k大元素在index右边(因为是按降序排的).
(3) 如果 k<index - left + 1,说明第k大元素在index左边.
(4) 找到了第k大, 说明k前面的元素都比k大. 也就是同时找到了最大的k个元素.
(5) 此方法也可以找 第k个数.