概念:
排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作
排序规则:
一般是升序降序排列,若待排序元素比较复杂,就会有更复杂的排序方式
常见排序算法:
插入排序基于顺序表的插入来实现的
直接插入排序的基本操作是将一个记录插入到已排好序的有序表里,从而得到一个新的、记录数+1的有序表
举例:
public static void insertSort(int[] array){
//通过bound来划分出两个区间
// [0,bound) 已排序区间
// [bound,size) 待排序区间
for (int bound = 1; bound < array.length; bound++) {
int v = array[bound];
// cur 表示已排序区间的最有一个元素下标
int cur = bound - 1;
for (; cur >= 0; cur--) {
if(array[cur] > v){
array[cur + 1] = array[cur];
}
// 不需要搬运
else {
break;
}
}
array[cur + 1] = v;
}
}
直接插入排序特点:
直接插入排序性能分析:
时间复杂度 —— O(N2)
空间复杂度 —— O(1)
稳定性: 稳定排序
进阶版本的插入排序
先分组,针对每个组进行(直接)插入排序,逐渐缩小组的个数,最终整个数组就有序
画图分析:
常见 gap 取值:size,size / 2,size / 4…1
代码实现:
//希尔排序
public static void shellSort(int[] array){
int gap = array.length / 2;
while (gap > 1){
// 需要循环进行分组插排
insertSortGap(array,gap);
gap = gap / 2;
}
insertSortGap(array,1);
}
private static void insertSortGap(int[] array, int gap) {
//当把 gap 换成1时,理论上该排序与插排一模一样
for (int bound = gap; bound < array.length; bound++) {
int v = array[bound];
//找同组中的上一个元素
int cur = bound - gap;
// 每次找同组中的相邻元素,同组元素中的下标差值就是gap
for (; cur >= 0; cur -= gap) {
//此处若带 =,那么插入排序就不稳定了
if(array[cur] > v){
array[cur + gap] = array[cur];
}
// 不需要搬运,此时说明找到了合适的位置
else {
break;
}
}
array[cur + gap] = v;
}
}
希尔排序性能分析:
时间复杂度 —— 理论极限:O(N1.3) 若按照 size / 2,size / 4…这种方式设置gap,则为: O(N2)
空间复杂度 —— O(1)
稳定性: 不稳定
基于打擂台的的思想,每次从数组中找出最小值,然后把最小值放在合适的位置
//选择排序
public static void selectSort(int[] array){
//[0,bound) 已排序区间
//[bound,size) 待排序区间
for (int bound = 0; bound < array.length; bound++) {
//以bound位置元素为 擂主,循环从待排序区间取出元素和擂主进行比较
for (int cur = bound + 1; cur < array.length; cur++) {
//若 打擂 成功,就和擂主交换位置
if(array[cur] < array[bound]){
int tmp = array[cur];
array[cur] = array[bound];
array[bound] = tmp;
}
}
}
}
选择排序性能分析:
时间复杂度 —— O(N2)
空间复杂度 —— O(1)
稳定性: 不稳定
选择排序思考非常好理解,但是效率不太好,实际中很少使用
升序排序:
代码实现:
//堆排序
public static void heapSort(int[] array){
//先建堆
createHeap(array);
//循环把堆顶元素交换到最后,同时向下调整堆
//当堆中只剩最后一个元素时,已有序,不需要再调整 循环次数:length-1
for (int i = 0; i < array.length - 1; i++) {
//交换堆顶元素和堆的最后一个元素
//堆的个数,相当于 array.length - i
//堆的最后一个元素下标为:array.length - i - 1
swap(array,0,array.length - i - 1);
//交换完成后,堆再次缩水 array.length - i - 1
//从堆中删除最后一个元素
//数组中
// [0,array.length-i-1) 待排序区间
// [array.length - i - 1,array.length) 已排序区间
//向下调整
shiftDown(array,array.length - i - 1,0);
}
}
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;
}
//child 是左右子树较大值的下标
if(array[child] > array[parent]){
swap(array,child,parent);
}
else{
break;
}
parent = child;
child = 2 * parent + 1;
}
}
private static void swap(int[] array,int i,int k){
int tmp = array[i];
array[i] = array[k];
array[k] = tmp;
}
堆排序的性能分析:
时间复杂度 —— O(N * logN)
空间复杂度 —— O(1)
稳定性: 不稳定
核心目标和堆排序、选择排序都很像,每次找到一个最大值 / 最小值,并放到一个合适的位置
借助相邻元素比较交换方式来找
代码实现:
//冒泡排序
public static void bubbleSort(int[] array){
for (int bound = 0; bound < array.length; bound++) {
// [0,bound) 待排序区间
// [bound,size) 已排序区间
for (int cur = 0; cur < bound; cur++) {
// 后一个 小于 前一个,就交换
if(array[cur] > array[cur + 1]){
swap(array,cur,cur + 1);
}
}
}
}
冒泡排序性能分析:
时间复杂度 —— O(N2)
空间复杂度 —— O(1)
稳定性: 稳定排序
依赖递归
注意:若取最右侧值为基准值的话,必须先从右往左找,后从左往右找
若取最左侧值为基准值的话,必须先从左往右找,后从右往左找
画图分析:
// 快速排序
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;
}
//针对 [left,right] 区间进行整理
// index 表示整理完成后,left 和 right 的重合位置
int index = partition(array,left,right);
quickSortHelper(array,left,index - 1);
quickSortHelper(array,index + 1,right);
}
private static int partition(int[] array, int left, int right) {
int begin = left;
int end = right;
//取最右侧元素为基准值
int base = array[right];
while (begin < end){
//从左往右找 > 基准值的
while (begin < end && array[begin] <= base){
begin++;
}
//循环结束后, begin 要么和 end 重合,要么指向一个 > base的值
//从右往左找 < 基准值的
while (begin < end && array[end] >= base){
end--;
}
//循环结束后, end 要么和 begin 重合,要么指向一个 < base的值
//交换 begin end 的值
swap(array,begin,end);
}
// 当 i k重合时,把重合位置的元素 与 基准值交换
swap(array,begin,right);
return begin;
}
快速排序性能分析:快速排序的效率与基准值取的好坏密切相关
若基准值是一个接近数组中位数的的元素,则划分出的左右区间就比较均衡,此时效率就比较高
若基准值是数组的最大 / 小值,则划分出的左右区间就不均衡,此时效率就低
若数组正好是反序,此时快排就变成了 "慢排"
最坏 时间复杂度: O(N2)
平均时间复杂度 —— O(N * logN)
平均空间复杂度 —— O(logN)
最坏空间复杂度 —— O(N)
稳定性: 不稳定排序
借助栈来模拟递归过程
代码实现:
//非递归实现快排
public static void quickSortByLoop(int[] array){
//借助栈,模拟实现递归的过程
//stack用来存放数组下标
Stack<Integer> stack = new Stack<>();
//初始情况下,先把右侧边界下标入栈,再把左侧边界下标入栈
// 左右边界 [ , ]
stack.push(array.length - 1);
stack.push(0);
while (!stack.isEmpty()){
//取出栈顶元素,取元素的顺序要和push的顺序相反
int left = stack.pop();
int right = stack.pop();
//只有 1个 或0 个元素,不需要整理
if(left >= right){
continue;
}
//通过 partition ,把区间整理成,左侧 ≤ 基准值,右侧 ≥ 基准值
int index = partition(array,left,right);
//准备处理下个区间
// [index+1,right) 基准值右侧区间
stack.push(right);
stack.push(index + 1);
// [left,index-1) 基准值右侧区间
stack.push(index - 1);
stack.push(left);
}
}
快速排序的优化:
.
1.优化基准值的取法 — 三个位置取中
最左侧元素,中间位置元素,最右侧元素,取中间值作为基准值,把确认的基准值交换到数组末尾或者开始位置
2.区间较小,直接插入排序
区间较小时,再去进行递归的话,效率较低,直接进行插入排序即可
3.区间特别大,使用堆排序
若区间特别大,递归的深度也会非常深,当递归深度到达一定程度时,把当前区间的排序使用堆排序来进行优化
归并排序有两个重要特点,可以适用于外部排序,也可以适用于链表排序
前边的几种排序,都是基于数字存在内存中,并且数据存在数组中,数据在内存中的,就叫内部排序
若数据存在磁盘中,那就是外部排序
希尔排序,堆排序,快速排序,都依赖于随机访问能力,不太适合针对链表排序
基本思路:
思路来源于经典问题,把两个有序链表 / 数组合并成一个
归并的前提是:两个待归并区间都是有序的
画图分析:
// 归并排序
public static void mergeSort(int[] array){
mergeSortHelper(array,0,array.length);
}
private static void mergeSortHelper(int[] array, int low, int high) {
//[low,high)
//区间只有 0个 或 1个 元素
if(high - low <= 1){
return;
}
int mid = (low + high) / 2;
mergeSortHelper(array,low,mid);
// 方法执行完, [low,mid) 排序完成
mergeSortHelper(array,mid,high);
// 方法执行完, [mid,high) 排序完成
// 两个区间已有序 针对两个有序区间进行合并
merge(array,low,mid,high);
}
// [low,mid) 有序区间
// [mid,high) 有序区间
// 把上述两个有序区间合并成一个有序区间
public static void merge(int[] array,int low,int mid,int high){
int[] output = new int[high - low];
// 记录当前output数组中放入多少个元素
int outputIndex = 0;
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];
}
}
归并排序性能分析:
时间复杂度 —— O(N * logN)
空间复杂度 —— O(N)
若针对链表归并,空间复杂度可以是:O(1)
稳定性: 稳定排序
代码实现:
public static void mergeSortByLoop(int[] array){
// 利用gap 变量进行分组
// 当 gap 为1时,[0] [1]进行合并,[2] [3]进行合并...
// 当 gap 为2时,[0,1]和[2,3]进行合并..[4,5]和[6,7]进行合并..
// 当 gap 为4时,[0,1,2,3]和[4,5,6,7]进行合并...
for (int gap = 1; gap < array.length; gap *= 2) {
//具体的分组 合并
for (int i = 0;i < array.length; i += 2 * gap){
//循环一次,就完成两个相邻组的合并
//相邻组
//[begin,mid) begin -> i
//[mid,end) mid -> i+gap
// end -> i + 2*gap
int begin = i;
int mid = i + gap;
int end = i + 2 * gap;
//防止下标越界
if(mid > array.length){
mid = array.length;
}
if(end > array.length){
end = array.length;
}
merge(array,begin,mid,end);
}
}
}
时间复杂度为:O(0),几乎不吃CPU
举例:9 5 2 7 3 6 8
取到9:创建线程,sleep(9),再打印 9
取到5:创建线程,sleep(5),再打印 5
.
.
.
sleep操作:让代码放弃CPU,偷懒睡觉~