目录
1. 排序
是的一串记录按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
一般情况下,采用原地排序,默认为升序。
- 稳定性
- 稳定
- 经过排序后,原来相同关键字的相对顺序不变。
- 不稳定
- 经过排序后,原来相同关键字的相对顺序可能发生改变。
- 稳定
- 内部排序
- 数据全部在内存中处理的排序
- 外部排序
- 对于数据元素较多的情况,必须在内外存之间移动数据的排序。
- 减治算法
- 每次减少一个数,剩下的数用同样的方法进行处理。
- 分治算法
- 每次将数列分区,每个区用同样的方法进行处理。
2. 常见的排序算法
-
2.1 直接插入排序 (减治)
- 把待排序的记录按其关键码值的大小逐个插入到一 个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
- 适用于当数组接近有序(大概率有序),数组的数量较小时(参考值20)
-
2.1.1 思路
- 设有序部分的最后一个下标为 j ,所要插入的数的下标为 i 。
- 有序部分[0,i)
- 无序部分[i,array.length)
- 1. 在有序部分,查找合适的位置
- 合适的位置存在3种情况
- a[ i ] > a[ j ] 找到合适的位置
- a[ i ] == a[ j ] 为了保证稳定性,认为找到合适的位置
- a[ i ] < a[ j ] j--
- 合适的位置存在3种情况
- 2. 将指定的数插入到合适的下标处
- 设有序部分的最后一个下标为 j ,所要插入的数的下标为 i 。
-
2.1.2 具体实现
- 1. 遍历查找
- 从前往后 / 从后往前
- 插入的位置:j+1
- 插入的过程相当于 给定 pos 的顺序表做插入
- 2. 二分查找
- 左闭右开查找,只需要对有序部分进行二分。且left==right时,区间中没有值。
- left=0
- right=i
- mid=left+(right-left)/2
- 插入位置:left
- 插入数字a[i]与a[mid]的关系
- a[mid]==a[i] / a[mid]<a[i] left=mid+1
- a[mid]>a[i] right=mid
- 1. 遍历查找
-
2.1.3 时间复杂度
- 最坏 O(n²) -> 逆序
- 平均 O(n²)
- 最好 O(n) -> 有序
-
2.1.4 空间复杂度
- 常数 O(1)
-
2.1.5 稳定性
- 稳定
-
2.1.6 代码
/**
* 插入排序
* Author:qqy
*/
public class InsertSort {
/**
* 遍历查找
* 先找位置后插入
* @param array
*/
public static void insertSort(int[] array){
for(int i=0;i<array.length;i++){
int val=array[i];
//在有序位置从后向前遍历查找
int j;
for(j=i-1;j>=0 && array[i]<array[j];j--){
}
//在j+1处插入
for(int k=i;k>j+1;k--){
array[k]=array[k-1];
}
array[j+1]=val;
}
}
/**
* 遍历查找 必会
* 边找位置边插入
* @param array
*/
public static void insertSort2(int[] array){
for(int i=0;i<array.length;i++) {
int val = array[i];
int j;
for (j = i - 1; j >= 0 && array[j] > val; j--) {
array[j + 1] = array[j];
}
array[j + 1] = val;
}
}
/**
* 二分查找
* @param array
*/
public static void insertSort3(int[] array){
for(int i=0;i<array.length;i++){
int val=array[i];
int left=0;
int right=i;
while(left<right){
int mid=left+(right-left)/2;
if(array[mid]>val){
right=mid;
}else {
left=mid+1;
}
}
for(int k=i;k>left;k--){
array[k]=array[k-1];
}
array[left]=val;
}
}
}
-
2.2 希尔排序
- 把待排序的记录进行多次分组插排,直到每组的个数为1。
- 分组插排
- 分组越多,最大数的步伐越大
- 分组越少,越接近有序
- gap:每组个数
- gap 从大到小,当gap==1,相当于直接插入排序。
- gap = length
- gap = (gap/3)+1
- gap=gap/2
-
2.2.1 思路
- 1. 将一组记录按照gap分组,将每组中的数据进行直接插入排序
- 2. 缩小gap -> 1
- 3. 直到gap==1
-
2.2.2 具体实现
- 1. 在直接插入排序的基础上进行修改,只比较组内数据
- 2. 利用循环进行分组比较,每次比较完,更改gap值
- 3. 当gap==1,跳出循环
-
2.2.3 时间复杂度
- 本质上还是直接插入排序
- 最坏 O(n²) -> 逆序 但是减少了最坏情况出现的概率
- 平均 O(n^1.2~1.3)
- 最好 O(n) -> 有序
- 本质上还是直接插入排序
-
2.2.4 空间复杂度
- 常数 O(1)
-
2.2.5 稳定性
- 不稳定,因为不能保证同样的数字放在同一个分组中。
-
2.2.6 代码
/**
* 希尔排序
* Author:qqy
*/
public class ShellSort {
private static void insertSortWithGap(int[] array, int gap) {
for(int i=0;i<array.length;i++){
int val=array[i];
int j;
for(j=i-gap;j>=0 && array[j]>val;j-=gap){
array[j+gap]=array[j];
}
array[j+gap]=val;
}
}
public static void shellSort(int[] array){
int gap=array.length;
while(true){
gap=(gap/3)+1;
insertSortWithGap(array,gap);
if(gap==1){
break;
}
}
}
}
-
2.3 选择排序 (减治)
-
每次选出最大的一个数,置于最后。
-
2.3.1 思路
- 1. 找到最大数,将最大数与无序部分的最后一个数进行交换
- 2. 找到最小数,将最小数与无序部分的第一个数进行交换
-
2.3.2 具体实现
- 每次选择之后
- 最大数
- 无序部分 [0,array.length - i)
- 有序部分 [array.length - i,array.length)
- 最小数
- 无序部分 [i,array.length)
- 有序部分 [0,i)
- 最大数
- 1. n个数需要选n次,最外层循环n次
- 2. 内层遍历找出无序部分最大数 / 最小数的下标
- 3. 将最大数 / 最小数与无序部分的最后一个 / 第一个数交换
- 每次选择之后
-
2.3.3 时间复杂度
- 无论是否有序,都需要先遍历找到最大值,然后进行交换
- 最好 O(n²)
- 平均 O(n²)
- 最坏 O(n²)
- 无论是否有序,都需要先遍历找到最大值,然后进行交换
-
2.3.4 空间复杂度
- 常数 O(1)
-
2.3.5 稳定性
- 不稳定
-
2.3.6 代码
-
/**
* 选择排序
* Author:qqy
*/
public class SelectSort {
//选择排序
public static void selectSort(int[] array) {
for(int i=0;i<array.length;i++){
int max=0;
//找出最大值
for(int j=1;j<array.length-i;j++){
if(array[max]<array[j]){
//不需要交换值,只需要记录最大值的位置就好
max=j;
}
}
int t=array[max];
array[max]=array[array.length-i-1];
array[array.length-i-1]=t;
}
}
}
-
2.4 堆排序 (减治)
-
每次选出最大的一个数,置于最后。
-
2.4.1 思路
- 1. 建最大堆
- 2. 将无序部分的最后一个数与根节点交换
- 3. 无序部分堆化 -> 2
-
2.4.2 具体实现
- 1. 利用向下调整建立最大堆
- 2. 利用循环不停交换无序部分的最后一个数和最大值(根结点)
- 3. 直至无序部分的最后一个数为根结点
-
2.4.3 时间复杂度
- 最好 O(n*log(n))
- 平均 O(n*log(n))
- 最坏 O(n*log(n))
-
2.4.4 空间复杂度
- 常数 O(1)
-
2.4.5 稳定性
- 不稳定,因为数字交换后的顺序无法保证。
-
2.4.6 代码
-
/**
* 堆排序
* Author:qqy
*/
public class HeapSort {
//堆排序
public static void heapSort(int[] array) {
//向下调整建大堆
createHeap(array);
for(int i=0;i<array.length;i++) {
int t = array[0];
array[0] = array[array.length - i - 1];
array[array.length-i-1]=t;
heapify(array,array.length-i-1,0);
}
}
public static void heapSort1(int[] array) {
createHeap(array);
for(int i=array.length-1;i>0;i--){
int t=array[i];
array[i]=array[0];
array[0]=t;
heapify(array,i,0);
}
}
public static void heapify(int[] array, int size, int index) {
while(2*index+1<size){
int max=2*index+1;
if(max+1<size && array[max+1]>array[max]){
max+=1;
}
if(array[max]<=array[index]){
break;
}
int t=array[max];
array[max]=array[index];
array[index]=t;
index=max;
}
}
private static void createHeap(int[] array) {
for (int i = (array.length - 2) / 2; i >= 0; i--) {
heapify(array, array.length, i);
}
}
}
-
2.5 冒泡排序 (减治)
-
从第一个数开始,两个相邻的数字依次比较,将大数向后推。
-
也可以从最后一个数开始,两个两个相邻的数字依次比较,将小数向前推。
-
2.5.1 思路
- 外部循环
- 经过一次冒泡过程,该数组中最大的数字一定放在了最后。
- 我们只需要将剩下的n-1个数字再次进行冒泡排序。
- 一共需要n次冒泡过程,但由于最后只剩一个数字的时候不需要进行比较,可以优化为实际只需要n-1次冒泡过程。
- 内部循环
- 数字两两比较,大的在后
- 第一次冒泡排序需要比较n个数字,也就是比较n-1次
- 每一次外部循环会使得需要比较的数字 -1
- 第二次冒泡排序需要比较n-1个数字,也就是比较n-2次
- ...
- 第 i 次冒泡排序需要比较n-1-i个数字,也就是比较n-i-2次
- 外部循环
-
2.5.2 优化
- 假设所给的数字已经有序,我们就不需要进行排序。因此,我们需要添加判断数字是否有序的部分。
- 如何判断是否有序:
- 当经过一次冒泡排序后,没有任何数字进行交换,则证明有序。
-
我们可以设置一个标记位来标记是否有数字进行交换。
- 如何判断是否有序:
- 假设所给的数字已经有序,我们就不需要进行排序。因此,我们需要添加判断数字是否有序的部分。
-
2.5.3 时间复杂度
- 最好 O(n) -> 有序
- 平均 O(n²)
- 最坏 O(n²) -> 逆序
-
2.5.4 空间复杂度
- 常数 O(1)
-
2.5.5 稳定性
- 稳定,因为array[j] > array[j + 1]
-
2.5.6 代码
-
/**
* 冒泡排序
* Author:qqy
*/
public class BubbleSort {
public static void bubbleSort(int[] array) {
//假设有序
boolean flag = true;
//外部循环一共需要n-1次冒泡排序
for (int i = 0; i < array.length - 1; i++) {
//外部循环第i次需要比较n-2-i次
for (int j = 0; j < array.length - 1 - i; j++) {
//将两个数字中较大的置后
if (array[j] > array[j + 1]) {
int t = array[j];
array[j] = array[j + 1];
array[j + 1] = t;
//进入到if中,则表明有数字进行交换,无序
flag = false;
}
}
//有序,退出循环
if (flag == true) {
break;
}
}
}
}
-
2.6 快速排序 (分治)
-
任取待排序元素序列中的某元素作为基准值,根据基准值将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值。左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
-
2.6.1 思路
- 1. 取得基准值
- 2. 将区内所有大于基准值的数置于右侧,小于基准值的数置于左侧。
- 3. 更改基准值 -> 2
-
2.6.2 具体实现
- 排序区间 array[ left , right ],排序一次后 基准值的下标为 pivotIndex
- 1. 将每个区中最右边的数作为基准值array[right]
- 2. 遍历整个区间
- 将区间分为三部分
- < 基准值 [ left , pivotIndex-1]
- == 基准值 privotIndex
- > 基准值 [pivotIndex+1 , right ]
- 如何分区
- a. Hover (左右遍历)
- 处理基准值以外的数据,设置begin(左边)和end(右边)标记。
- 比较
- array[begin] <= 基准值 ,begin++;
- array[end] >= 基准值 ,end--;
- 否则交换 array[begin] 和 array[end]
- 直至begin和end相遇
- 将基准值与相遇位置处的值进行交换
- 因为
- 先比较左区,再比较右区(否则第一个数有可能没有参与比较)
- 所以
- ① array[begin] 一定大于基准值
- ② begin与end相遇处(array[end] 是与begin交换后的数字)的数字一定大于基准值
- 因为
- b. 挖坑 (左右遍历)
- 处理基准值以外的数据,设置begin(左边)和end(右边)标记。
- 比较
- 将基准值取出,则其位置为“坑”
- 移动begin
- array[begin] <= 基准值 ,begin++;
- 否则,将值放于end处(坑) -> 移动end
- 移动end
- array[end] >= 基准值 ,end--;
- 否则,将值放于begin处(坑) -> 移动begin
- 直至begin和end相遇
- 将基准值放入begin处
- c. 前后下标 (单向遍历)
- 处理基准值以外的数据,设置small(左边)和big(左边)标记。
- 比较
- 保证小于基准值的数在[0 , small),大于基准值的数在[small , big]
- 移动下标
- array[big] < 基准值
- 交换array[big] 和 array[small]
- small++
- big++
- 否则,big++
- 直到big == right
- array[big] < 基准值
- 交换array[small] 和基准值
- 左右分区继续进行数据处理
- a. Hover (左右遍历)
- 将区间分为三部分
-
2.6.3 时间复杂度
- 每次分区的时间复杂度为O(n),而需要进行的分区次数为log(n) ~ n
- log(n) -> 看做二叉树,n个数一共有log(n)层
- n -> 当二叉树为单支树时,n个树就有n层
- 最好 O(n*log(n))
- 平均 O(n*log(n))
- 最坏 O(n²) -> 当数组已经有序或者数组逆序时
- 每次分区的时间复杂度为O(n),而需要进行的分区次数为log(n) ~ n
-
2.6.4 优化
- 为了避免单支树的出现,可以改变基准值的取法
- 随机法 -> radom.nextInt(4)
- 三数取中法 -> 选择左边、中间以及右边的数,比较大小,取中间大小的数字
- 将基准值交换至最右边,即可使用之前的方法进行快速排序。
- 为了避免单支树的出现,可以改变基准值的取法
-
2.6.5 空间复杂度
- 二叉树的高度
- 最好 O(log(n))
- 平均 O(log(n))
- 最坏 O(n)
- 二叉树的高度
-
2.6.6 稳定性
- 不稳定
-
2.6.7 代码
-
/**
* 快速排序
* Author:qqy
*/
public class QuickSort {
public static void quickSort(int[] array,int left,int right){
if(left>=right){
return;
}
int pivot=theMiddleOfThreeNumbers(array,left,right);
swap(array,right,pivot);
//获取每次排序后,基准值的位置
int pivotIndex=partitionIndex(array,left,right);
// int pivotIndex=partitionHover(array,left,right);
// int pivotIndex=partitionPit(array,left,right);
//小于基准值区间[left,pivotIndex-1]
quickSort(array,left,pivotIndex-1);
//大于基准值区间[pivotIndex+1,right]
quickSort(array,pivotIndex+1,right);
}
//分区方法 -> hover
public static int partitionHover(int[] array,int left,int right){
int begin=left;
int end=right;
int pivot=array[right];
//直到两者相遇
while(begin<end) {
while (begin<end && array[begin] <= pivot) {
begin++;
}
while (begin<end && array[end] >= pivot) {
end--;
}
swap(array,begin,end);
}
swap(array,begin,right);
return begin;
}
public static void swap(int[] array,int a,int b){
int t=array[a];
array[a]=array[b];
array[b]=t;
}
//分区方法 -> 挖坑
public static int partitionPit(int[] array,int left,int right){
int begin=left;
int end=right;
int pivot=array[right];
while(begin<end){
while(begin<end && array[begin]<=pivot){
begin++;
}
array[end]=array[begin];
while(begin<end && array[end]>=pivot){
end--;
}
array[begin]=array[end];
}
array[begin]=pivot;
return begin;
}
//分区方法 -> 前后下标
public static int partitionIndex(int[] array,int left,int right){
int small=left;
int big=left;
int pivot=array[right];
while(big<right) {
if (array[big] < pivot) {
swap(array, small, big);
small++;
}
big++;
}
swap(array,small,right);
return small;
}
//三数取中法
public static int theMiddleOfThreeNumbers(int[] array,int left,int right){
int mid=left+(right-left)/2;
if(array[left]<array[right]){
if(array[left]>array[mid]){
return left;
}else if(array[right]<array[mid]){
return right;
}
}else{
if(array[left]<array[mid]){
return left;
}else if(array[right]>array[mid]){
return right;
}
}
return mid;
}
}
-
2.7 归并排序 (分治)
- 先使每个子序列有序,再将两个有序表合并成一个有序表,称为二路归并。
- 外部排序最好的算法
- 需要使用外部排序的情况
- 内存放不下
- 步骤
- 先把数据切割成内存放的下的大小(n份)
- 对每一份进行排序
- 合并n个有序数组(归并)
- 需要使用外部排序的情况
-
2.7.1 思路
- 1. 将排序区间平均切分为两个部分
- 2. 对两个区间进行排序(递归)
- 3. 合并两个有序区间成为一个有序区间(需要额外空间)
-
2.7.2 具体实现
- 1. 将排序区间平均分为两个区间 [low,mid) 、[mid,high)
- 2. 两个区间递归调用mergeSort / 非递归:1个数merge()、2个数merge()、4个数merge()、8个数merge()... ...
- 3. 直至区间内只有1个数或没有数
- 4. 创建一个额外空间,存放合并后的区间
- 如何合并
- 设置两个变量 left,right分别遍历两个区间
- 比较array[left] 和 array[right]
- 将较小的放入额外空间,下标向后走
- 继续比较
- 直到任一区间为空,将另一个区间的剩余值直接放入额外空间
- 将额外空间的值返还给原空间
- 如何合并
-
2.7.3 时间复杂度
- 最好 O(n*log(n))
- 平均 O(n*log(n))
- 最坏 O(n*log(n))
-
2.7.4 空间复杂度
- O(n) + O(log(n)) -> O(n)
-
2.7.5 稳定性
- 稳定
-
2.7.6 代码
/**
* 归并排序
* Author:qqy
*/
public class MergeSort {
//非递归
public static void mergeSortNorR(int[] array) {
int[] extra = new int[array.length];
for(int i=1;i<array.length;i*=2){
for(int j=0;j<array.length;j+=2*i){
int low=j;
int mid=j+i;
//若没有右区间
if(mid>array.length){
mid= array.length;
}
int high=mid+i;
//若右区间越界
if(high>array.length){
high= array.length;
}
merge(array,low,mid,high,extra);
}
}
}
//递归
public static void mergeSort(int[] array) {
int[] extra = new int[array.length];
//左闭右开
mergeSortInner(array, 0, array.length, extra);
}
public static void mergeSortInner(int[] array, int low, int high, int[] extra) {
if (low >= high - 1) {
return;
}
int mid = low + (high - low) / 2;
//左区间 [low,mid) 有区间 [mid,high)
mergeSortInner(array, low, mid, extra);
mergeSortInner(array, mid, high, extra);
//合并两个有序区间
merge(array, low, mid, high, extra);
}
public static void merge(int[] array, int low, int mid, int high, int[] extra) {
int left = low;
int right = mid;
//x标记额外空间的下标
int x = 0;
while (left < mid && right < high) {
if (array[left] <= array[right]) {
extra[x++] = array[left++];
} else {
extra[x++] = array[right++];
}
}
//若第一个区间有剩余,直接加入
while (left < mid) {
extra[x++] = array[left++];
}
//第二个队列有剩余
while (right < high) {
extra[x++] = array[right++];
}
//返还给原数组
for (int k = low; k < high; k++) {
array[k] = extra[k - low];
}
}
}
3. 总结
排序方法 | 概念 | 算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 数据敏感性 | |||
---|---|---|---|---|---|---|---|---|---|
最好 | 平均 | 最坏 | 最好 / 平均 | 最坏 | |||||
直接插入排序 | 获取无序部分任一数, 插入有序部分合适位置 | 减治 | O(n) | O(n²) | O(n²) | O(1) | 稳定 | 敏感 | |
希尔排序 | 分组插入排序, 直至组内只有一个数 | 特殊 | O(n) | O(n^1.2) | O(n²) | O(1) | 不稳定 | 敏感 | |
选择排序 | 获取无序中的最大数, 放入无序部分的最后 | 减治 | O(n²) | O(1) | 不稳定 | 不敏感 | |||
堆排序 | 建立最大堆, 交换根结点与无序部分的最后一个结点的值, 再堆化 | 减治 | O(n*log(n)) | O(1) | 不稳定 | 不敏感 | |||
冒泡排序 | 狗熊掰玉米 -> 当遇到较大的玉米, 扔掉旧的, 抱着新的继续掰 | 减治 | O(n) | O(n²) | O(n²) | O(1) | 稳定 | 敏感 | |
快速排序 | 利用三种方式获取基准值, 将基准值与最后一个数交换。 根据基准值分区, 小于基准值置左; 大于基准值置右 | 分治 | O(n*log(n)) | O(n*log(n)) | O(n²) | O(log(n)) | O(n) | 不稳定 | 敏感 |
归并排序 | 均分数据, 两个区间分别排序, 合并两个有序区间 | 分治 | O(n*log(n)) | O(n) | 稳定 | 不敏感 |