(一).排序的概念
1.概念
排序 :所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性 :假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j] ,且 r[i] 在 r[j] 之前,而在排序后的序列中, r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的。

例如上面的图片,排序前红色的"6"在黑色的"6"后面,当排序完成后,红色的"6"依然在黑色的"6"后面,此时就说这个排序是稳定的,当红色的"6"在黑色的"6"前面,此时就说这个排序是不稳定的。
注意:如果一个排序算法是稳定的,那么可以变成不稳定的,如果一个排序算法本身就不是稳定的,那么就不可能变成一个稳定的排序算法。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
2.常见的排序算法:

(二).排序算法的实现
1.插入排序
(1).基本思想
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。例如玩扑克牌,就是运用了插入排序的思想

(2).直接插入排序
- 分区初始化 :将数组天然划分为「有序区间」和「未排序区间」,默认数组下标 0 的元素为有序区间的初始值;
- 遍历未排序区间 :从数组下标 1 的位置开始,逐个取出「未排序区间」的元素作为待插入元素;
- 缓存待插入元素:提前把当前待插入元素缓存起来,避免后续元素后移时被覆盖,保证数据不丢失;
- 向前查找插入位置 :以内层循环从「有序区间的最后一个元素」向前遍历比较;
- ✔ 若有序区间的元素 > 待插入元素 → 该有序元素向后移动一位,为待插入元素「腾出插入空间」;
- ✔ 若有序区间的元素 ≤ 待插入元素 → 该有序元素的下一个位置,就是待插入元素的「目标位置」;
- 完成插入:将缓存好的待插入元素,放入找到的目标位置,此时有序区间长度 + 1;
- 重复执行:直至「未排序区间」的所有元素都完成插入,整个数组完全有序。

java
public static void insertSort(int[] array){
//外层循环:遍历「未排序区间」的所有元素,i是未排序元素的起始下标
for (int i = 1; i < array.length; i++) {
//缓存当前要插入的未排序的元素(避免后移的时候将元素覆盖)
int temp=array[i];
//j指向「已排序区间」的最后一个元素(当前待插入元素的前一个位置)
int j=i-1;
for(;j>=0;j--){
//当前已排序元素比待插入元素大,需要后移
if (array[j]>temp){
//则将当前值赋值给当前值的后一个值
//为temp腾出空间
array[j+1]=array[j];
}else{
//找到插入位置(j的下一位),直接插入temp
array[j+1]=temp;
break;
}
}
//兜底赋值:处理「待插入元素是已排序区间最小值(数组首元素)」的特殊情况
array[j+1]=temp;
}
}



特性总结:
时间复杂度:O(N^2)
最坏的情况:数组是逆序的,例如:5,4,3,2,1
当i等于1 的时候,j执行了1次,当i=2的时候,j执行了2次,当i等于n-1的时
候,j执行了n-1次,由此看来,是一个等差数列,所以时间复杂度是O(N^2)
最好的情况:数组本身是顺序的,例如1,2,3,4,5,时间复杂度可以达到
O(N)
所以数组越有序,直接插入排序越快
空间复杂度:O(1)
直接在原数组上进行操作,没有额外开辟内存,所以空间复杂度是O(1)
稳定性:稳定的排序
java
for(;j>=0;j--){
//当前已排序元素比待插入元素大,需要后移
if (array[j]>=temp){
//则将当前值赋值给当前值的后一个值
//为temp腾出空间
array[j+1]=array[j];
}else{
//找到插入位置(j的下一位),直接插入temp
array[j+1]=temp;
break;
}
}
当我将**if (array[j]>temp)改成if (array[j]>=temp)**之后,就是一个不稳定的排序了
所以 如果一个排序算法是稳定的,那么可以变成不稳定的,如果一个排序算法本身就不是稳定的,那么就不可能变成一个稳定的排序算法。
(3).希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是: 先选定一个整数,把待排序文件中所有记录分成多个组, 所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达 =1 时,所有记录在统一组内排好序 。

通过图片中的过程可以看出,我们是先分组,然后在组内进行直接插入排序。
注意:希尔排序的分组方式是跳跃性的分组,每组数据进行轮流排序,希尔排序的会把大的数据越来越靠后,小的数据越来越靠前
所以希尔排序可以看作是直接插入排序的一种优化。
之所以希尔排序叫做缩小增量法,是因为刚开始分的组很多,但是每组的数据很少,数据无序,所以相对应直接插入排序的时候时间复杂度O(N)也就变小了,慢慢的当组数越来越少的时候,每组的数据变多了,但是数据趋于有序,所以时间复杂度O(N)也就变小了
注意:不管前面怎么排序, 最终都整体看作一组进行直接插入排序 ,也就是说,当 组数>1 的时候,都是 预排序
java
public static void shellSort(int[] array){
//初始步长设置为数组长度
int gap= array.length;
while (gap>1){
//步长每次都折半
gap/=2;
//按照当前步长对数组进行分组直接插入排序
shell(array,gap);
}
}
private static void shell(int[] array, int gap) {
//从第gap个元素开始,逐个对其所在的子序列进行插入排序
for (int i = gap; i < array.length ; i++) {
int temp=array[i];
//定位到当前元素的前一个同组元素(步长gap)
int j = i-gap;
for (; j >=0 ; j-=gap) {
//前一个元素比temp大
if (array[j]>temp){
//前一个元素后移
array[j+gap]=array[j];
}else{//找到小于等于temp的元素,停止遍历
array[j+gap]=temp;
break;
}
}
//处理j=-1的情况,所有前序元素都比temp大
array[j+gap]=temp;
}
}
注意:for (int i = gap; i < array.length ; i++) 和for (; j >=0 ; j-=gap)
即使i++是不同组别,i++也能进行排序,只不过是每次对不同的组进行直接插入排序


通过上面的两个图片,我们可以好理解一点
特性总结:
希尔排序是对直接插入排序的一种优化
当 gap>1的时候,都是预排序 ,是为了让数据更接近于有序,当 gap=1的时候,数据已经趋于有序了,此时使用直接插入排序就会很快,从而达到优化效果
时间复杂度:不好计算,大致上是n^1.3~n^1.5
根据gap的取值不同,时间复杂度也不相同,好多书中也给出了不同的看法
《数据结构 (C 语言版 ) 》 --- 严蔚敏
《数据结构 - 用面向对象方法与 C++ 描述》 --- 殷人昆
空间复杂度:O(1)
稳定性:不稳定的排序
2.选择排序
(1).基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
(2).直接选择排序
将数组分为"已排序区间"和"未排序区间",每一轮从未排序区间中找到最小元素,将其与未排序区间的第一个元素交换位置,逐步扩大已排序区间,直到整个数组有序。

java
public static void selectSort(int[] array){
//外层循环:控制已排序区间的边界(i是未排序区间的第一个元素下标)
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;
}
}
//将找到的最小值和未排序区间的第一个元素交换
swap(array,minIndex,i);
}
}
private static void swap(int[] array, int j, int i) {
int temp=array[i];
array[i]=array[j];
array[j]=temp;
}
特性总结:
时间复杂度:O(N^2)
当i=0的时候,j执行n-1次,当i=1的时候,j执行了n-2次·······当i=n-1时,j执行了0
次,所以时间复杂度时O(N^2),同时可以看出来,不管数据是否有序,直接选择
排序都是这样
执行,所以直接选择排序的时间复杂度与数据本身是否有序或无序无关
空间复杂度:O(1)
稳定性:不稳定的排序
(3).优化后的直接选择排序
可以通过双向选择排序来提高我们的效率

但是还有一个问题!!!
java
public static void selectSort(int[] array){
int left=0;
int right=array.length-1;
//只要左边界小于右边界,说明区间内还有未排序元素
while (left<right){
//发呢别初始化最大值和最小值为左边界
int maxIndex=left;
int minIndex=left;
//遍历当前区间[left+1,right],找最小/最大值的下标
for (int i = left+1; i <=right ; i++) {
//更新最小值下标
if (array[i]<array[minIndex]){
minIndex=i;
}
//更新最大值下标
if (array[i]>array[maxIndex]){
maxIndex=i;
}
}
//把最小值交换到当前区间的最左端
swap(array,left,minIndex);
//如果最大值原本就在left位置,上面的swap已经把他换到了minIndex位置
//因此需要更新maxIndex,否则后续交换最大值会出错
if (maxIndex==left){
maxIndex=minIndex;
}
//把最大值交换到当前区间的最右端
swap(array,right,maxIndex);
//缩小排序区间
left++;
right--;
}
}
private static void swap(int[] array, int j, int i) {
int temp=array[i];
array[i]=array[j];
array[j]=temp;
}
(4).堆排序
首先将数组创建成大根堆,然后定义一个end变量为最后一个节点的下标,然后将end对应的值和堆顶元素进行交换,交换完成之后让end--,同时将end之前的所有节点接着调成大根堆,直到end=0的时候,才调整完毕由于堆排序之前已经介绍过了,不了解的可以看
堆排序
这里直接上代码
java
public static void heapSort(int[] array){
//创建初始大根堆
creatHeap(array);
//让end表示最后一个节点的下标
int end=array.length-1;
while (end>0){
//每次都将堆顶元素和end对应的元素进行交换
swap(array,0,end);
//将交换完的前end个元素调整为新的大根堆[0,end-1]
siftDown(array,0,end);
//缩小未排序的区间
end--;
}
}
//构建大根堆
private static void creatHeap(int[] array) {
//找到最后一个节点的根节点
for (int parent = (array.length-1-1)/2; parent >= 0; parent--) {
// 调整的区间是 [0, array.length-1]
siftDown(array,parent,array.length);
}
}
private static void siftDown(int[] array, int parent,int end) {
int child=2*parent+1;//左孩子的下标
//确保孩子节点在调整区域内
while (child<end){
//首先先找出左右孩子的最大值
if (child+1<end){
if (array[child]<array[child+1]){
child++;
}
}
//判断child下标的值是否大于parent下标的值
if (array[child]>array[parent]){
swap(array,child,parent);
//父亲节点下移到孩子节点的位置
parent=child;
//重新计算左孩子的下标
child=2*parent+1;
}else{
break;
}
}
}
private static void swap(int[] array, int j, int i) {
int temp=array[i];
array[i]=array[j];
array[j]=temp;
}
特性总结:
时间复杂度:O(N*logN)
向下调整的时间复杂度为O(N),换一次的时间复杂度为O(log(N)),换一次的
循环复杂度为(n-1)*log(n),整体复杂度为O(N*log(N))
空间复杂度:O(1)
稳定性:不稳定的排序
3.交换排序
(1).基本思想
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
(2).冒泡排序
重复遍历待排序的数组,每次比较相邻的两个元素,如果顺序错误(比如升序排序时前一个比后一个大)就交换它们,直到整个数组中没有需要交换的元素为止。
由于冒泡排序比较简单,所以直接上 优化的代码
java
public static void bubbleSort(int[] array){
//外层循环控制比较的趟数
for (int i = 0; i < array.length-1; i++) {
//定义一个boolean类型的变量,如果j遍历完以后,flag依旧为false
//说明数组中的元素已经有序了,则直接跳出循环即可
boolean flag=false;
//每一趟需要比较的次数
for (int j = 0; j < array.length-1-i; j++) {
//如果前一个比后一个大,则交换
if (array[j]>array[j+1]){
swap(array,j,j+1);
flag=true;
}
}
//本趟未交换,说明数组已经完全有序,则直接退出
if (!flag){
break;
}
}
}
特性总结:
时间复杂度:O(N^2),
最坏的情况,逆序的时候,5,4,3,2,1
最好的情况,顺序的时候,1,2,3,4,5 时间复杂度可以达到O(N)
空间复杂度:O(1)
稳定性:稳定的排序
(3).快速排序
i.Hoare法
快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为: 任取待排序元素序列中的某元 素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有 元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止 。
从后面开始找比基准值小的值停下来
从前面开始找比基准值大的值停下来
java
public static void quickSort(int[] array){
quick(array,0,array.length-1);
}
private static void quick(int[] array, int start, int end) {
if (start>=end){
return;
}
int pivot=partition(array,start,end);
//递归排序左区间(小于基准)
quick(array,start,pivot-1);
//递归排序右区间(大于基准)
quick(array,pivot+1,end);
}
private static int partition(int[] array, int left, int right) {
//选择左边界为基准值
int temp=array[left];
//记录基准原始位置
int tempLeft=left;
//双指针相向遍历
while (left<right){
//右指针左移:找小于基准值的元素(必须先移右指针!)
while (left<right&&array[right]>=temp){
right--;
}
//左指针右移:找大于基准值的元素
while (left<right&&array[left]<=temp){
left++;
}
//交换找到的两个不符合规则的元素
swap(array,left,right);
}
//循环结束:left == right,将基准值归位到正确位置
swap(array,left,tempLeft);
//返回基准最终位置
return left;
}
为什么要让右指针先走?

上面的图片是先左后右的结果,显然是不正确的
我们之所以是先右后左,是因为我们的基准值选取在最左侧,而且我们要保证指针相遇位置的元素小于等于基准值,同时在换完之后,我们要 保证在基准值左侧的元素都比基准值小,在基准值右侧的元素都比基准值大
当我们选取的基准值在最右侧,此时我们就要先左后右
为什么要取到等号?

当一组数据是这样的时候
java
//右指针左移:找小于基准值的元素(必须先移右指针!)
while (left<right&&array[right]>temp){
right--;
}
//左指针右移:找大于基准值的元素
while (left<right&&array[left]<temp){
left++;
}
不取等号的时候,上面的循环一直进不去,所以最左侧的7和最右侧的7一直在交换,根本无意义。
同时,
java
while (left<right){
//右指针左移:找小于基准值的元素(必须先移右指针!)
while (left<right&&array[right]>=temp){
right--;
}
//左指针右移:找大于基准值的元素
while (left<right&&array[left]<=temp){
left++;
}
//交换找到的两个不符合规则的元素
swap(array,left,right);
}
为什么在外部循环中,已经加上left<right的限制条件了,在内层循环中依然要加上left<right?

如上图所示,当我left想要找一个比8大的数据的时候,left会不断的++,直到left + 到了right也没有找到比8大的数据,此时left再++,就会数组越界
特性总结:
注意: 当讨论快速排序的时间复杂度,空间复杂度,稳定性的时候,默认讨论的是 最好的情况 ,因为 快速排序本身就不适合对已经有序的数据进行排序 ,同时我们通常会将快速排序进行 优化。
时间复杂度:O(NlogN)
首先要明确一点,当排序的数据已经是有序的时候,例如 1,2,3,4,5
或者5,4,3,2,1的时候,创建的是一颗单分支的树,所以最坏情况下的
时间复杂度是O(N^2)
最好的情况就是待排序的数据类似于一颗满二叉树的时候,
每一层都需要对N个元素进行排序,调整的层数就是二叉树的高度(O(logN)),
时间复杂度就是O(N*logN)
空间复杂度:O(logN)
最坏的情况就是数据已经有序,创建的是一棵单分支的树,所以空间复杂度是
O(N)
最好的情况就是类似于一棵满二叉树,当递归树的右侧的时候,左侧的递归空
间已经被回收了,所以空间复杂度是O(logN)
稳定性:不稳定的排序
ii.挖坑法
1.选取当前区间 最左侧的元素作为基准值 ,并将这个值保存下来。(此时 最左侧位置就形成了第一个[坑])。
2.从 当前区间 的 最右侧开始向左遍历 ,找到 第一个小于基准值 的元素,将 这个元素填入到左侧的坑 中,此时 该元素原来的位置就成为了新的坑。
3.接着 从刚填入坑的位置从左向右找到第一个大于基准值的元素 , 将这个元素填入到右侧的坑中,此时该元素原来的位置又形成了新的坑。
4.重复步骤2和3,直到 左右指针(left/right)相遇。
5.将 刚开始保存的基准值填入到left和right相遇的这个坑中,此时基准值的位置就是其最终排序的位置(左侧全<=基准值,基准值>=右侧全)
- 递归处理基准值左侧的子区间和右侧的子区间,直到所有子区间长度为1

java
public static void quickSort(int[] array){
quick(array,0,array.length-1);
}
private static void quick(int[] array, int left, int right) {
//递归终止条件,子区间只有1个元素或无元素,无需排序
if (left>=right){
return;
}
//找到基准值的最终位置
int pivot=partition(array,left,right);
//递归处理基准值的左侧子区间
quick(array, left, pivot-1);
//递归处理基准值的右侧子区间
quick(array, pivot+1, right);
}
private static int partition(int[] array, int start, int end){
//选最左元素为基准值,挖第一个坑
int temp=array[start];
//循环填坑,直到start和end相遇
while (start<end){
//从右向左找<基准值的元素,填左坑
while (start<end&&array[end]>=temp){
end--;
}
//找到小于基准值的元素,填入start坑
array[start]=array[end];
//从左向右找>基准值的元素,填右坑
while (start<end&&array[start]<=temp){
start++;
}
//找到大于7的元素,填入end坑
array[end]=array[start];
}
//指针相遇,将基准值填入最后一个坑
array[start]=temp;
//返回基准值的最终位置
return start;
}
特性总结:
时间复杂度:O(N*logN)
最坏的情况是O(N^2),就是当数据本身就是有序的或者逆序的,挖坑法相对
于Hoare法来说,省去了交换的步骤,从而提高了时间效率
最好的情况是O(NlogN),类似于完全二叉树,每一次递归都能平分数据
空间复杂度:O(logN)
最坏的情况是O(N),当数据本身就是有序或者逆序的时候,创建的是一颗单
分支的树,每次递归都创建,所以是O(N)
最好的情况就是O(logN),因为当递归二叉树的右子树的时候,说明左子树已
经递归完了,此时回收递归左子树的空间,所以空间复杂度是O(logN)
稳定性:不稳定的排序
iii.前后指针法
- 选最左元素为基准值;
- 用
prev标记 "小于基准值区域的最后一个位置",cur遍历数组;
- 遍历过程中,把小于基准值的元素逐步交换到
prev右侧,最终将基准值交换到prev位置,实现分区。
java
public static void quickSort(int[] array){
quick(array,0,array.length-1);
}
private static void quick(int[] array, int left, int right) {
//递归终止条件,子区间只有1个元素或无元素,无需排序
if (left>=right){
return;
}
//找到基准值的最终位置
int pivot=partitionTwoPointers(array,left,right);
//递归处理基准值的左侧子区间
quick(array, left, pivot-1);
//递归处理基准值的右侧子区间
quick(array, pivot+1, right);
}
private static int partitionTwoPointers(int[] array, int start, int end){
//小于基准值区域的最后一个位置,初始指向基准值
int prev=start;
//cur为遍历指针,从基准值下一个位置开始
int cur=prev+1;
//遍历整个区间(从start+1到end)
while (cur<=end){
//当前元素<=基准值(需要划入左侧区域) 避免相同元素无效交换
if (array[cur]<=array[start]&&array[++prev]!=array[cur]){
swap(array,prev,cur);
}
cur++;//遍历下个元素
}
//将基准值交换到prev位置(左侧全<=基准值,右侧全>=基准值)
swap(array, start,prev);
return prev;
}
特性总结:
时间复杂度:O(N*logN)
最坏的情况是O(N^2),就是当数据本身就是有序的或者逆序的,挖坑法相对
于Hoare法来说,省去了交换的步骤,从而提高了时间效率
最好的情况是O(NlogN),类似于完全二叉树,每一次递归都能平分数据
空间复杂度:O(logN)
最坏的情况是O(N),当数据本身就是有序或者逆序的时候,创建的是一颗单
分支的树,每次递归都创建,所以是O(N)
最好的情况就是O(logN),因为当递归二叉树的右子树的时候,说明左子树已
经递归完了,此时回收递归左子树的空间,所以空间复杂度是O(logN)
稳定性:不稳定的排序
iv.快速排序优化
(1).三数取中法
1.选取三个侯选位置的元素: 数组当前区间的左边界下标left,右边界下标right,中间下标mid=(left+right)/2。
2.找出这三个位置元素的 中位数。
3.将这个 中位数 对应的元素下标与 left下标位置的元素交换
- 将交换后left下标位置的元素作为基准值,继续执行快速排序的分区逻辑
java
public static void quickSort(int[] array){
quick(array,0,array.length-1);
}
private static void quick(int[] array, int left, int right) {
if (left>=right){
return;
}
//找左、中、右三个位置的中位数下标
int mid=findMidNum(array,left,right);
//将中位数交换到左边界(作为基准值的候选)
swap(array,mid,left);
int par=partition(array, left, right);
quick(array,left,par-1);
quick(array,par+1,right);
}
//取出这三个数,然后找出这三个数中的中位数,然后将这个中位数与下标为left的数进行交换
//此时就类似于满二叉树了
private static int findMidNum(int[] array, int left, int right) {
int mid=(left+right)/2;
if (array[left]>array[right]){
//如果说left下标的值大于right下标的值,同时left下标的值小于mid下标的值,那么返回left
//例如 array[left]=9 array[right]=4 array[mid]=20
//4 9 20 所以返回left
if (array[left]<array[mid]){
return left;
}else if (array[right]>array[mid]){
//如果left下标的值大于right下标的值,并且right下标的值大于mid下标的值
//例如 array[left]=9 array[right]=4 array[mid]=2
//2 4 9 所以返回right
return right;
}else{
return mid;
}
}else {
if (array[left]>array[mid]){
//如果left下标的值小于right下标的值,同时mid下标的值小于left下标的值,则返回left
//例如 array[left]=4 array[right]=9 array[mid]=2
// 2 4 9 则返回left
return left;
}else if(array[mid]>array[right]){
//如果left下标的值小于right下标的值,同时mid下标的值大于right下标的值
//例如 array[left]=4 array[right]=9 array[mid]=20
// 4 9 20 则返回right
return right;
}else{
return mid;
}
}
}
private static int partition(int[] array, int start, int end){
//选最左元素为基准值,挖第一个坑
int temp=array[start];
//循环填坑,直到start和end相遇
while (start<end){
//从右向左找<基准值的元素,填左坑
while (start<end&&array[end]>=temp){
end--;
}
//找到小于基准值的元素,填入start坑
array[start]=array[end];
//从左向右找>基准值的元素,填右坑
while (start<end&&array[start]<=temp){
start++;
}
//找到大于7的元素,填入end坑
array[end]=array[start];
}
//指针相遇,将基准值填入最后一个坑
array[start]=temp;
//返回基准值的最终位置
return start;
}
通过三数取中法,来避免递归深度达到最坏情况,从而让递归深度保持在最平均水平
(2).递归到小的子区间时,可以考虑使用插入排序
这个也比较好理解,快速排序是越来越有序,而当数组越有序,直接插入排序越快。所以我们可以进行一下判断,当未排序的区间小于一定的数目的时候,我们可以直接调用直接插入排序,即不用递归了,可以减少递归所开辟的空间。
java
public static void quickSort(int[] array){
quick(array,0,array.length-1);
}
private static void quick(int[] array, int left, int right) {
if (left>=right){
return;
}
//当带排序区间的个数小于7个的时候,直接使用直接插入排序
if (right-left+1<=7){
insertSort(array,left,right);
return;
}
//找左、中、右三个位置的中位数下标
int mid=findMidNum(array,left,right);
//将中位数交换到左边界(作为基准值的候选)
swap(array,mid,left);
int par=partition(array, left, right);
quick(array,left,par-1);
quick(array,par+1,right);
}
private static void insertSort(int[] array, int left, int right) {
for (int i = left+1; i <=right ; i++) {
int temp=array[i];//待插入的元素
int j=i-1;//向前比较的指针
for (; j >=left ; j--) {
if (array[j]>temp){
array[j+1]=array[j];//元素后移
}else{
array[j+1]=temp;//找到插入位置,放入temp
break;
}
}
array[j+1]=temp;//处理j走到left=1的情况
}
}
//取出这三个数,然后找出这三个数中的中位数,然后将这个中位数与下标为left的数进行交换
//此时就类似于满二叉树了
private static int findMidNum(int[] array, int left, int right) {
int mid=(left+right)/2;
if (array[left]>array[right]){
//如果说left下标的值大于right下标的值,同时left下标的值小于mid下标的值,那么返回left
//例如 array[left]=9 array[right]=4 array[mid]=20
//4 9 20 所以返回left
if (array[left]<array[mid]){
return left;
}else if (array[right]>array[mid]){
//如果left下标的值大于right下标的值,并且right下标的值大于mid下标的值
//例如 array[left]=9 array[right]=4 array[mid]=2
//2 4 9 所以返回right
return right;
}else{
return mid;
}
}else {
if (array[left]>array[mid]){
//如果left下标的值小于right下标的值,同时mid下标的值小于left下标的值,则返回left
//例如 array[left]=4 array[right]=9 array[mid]=2
// 2 4 9 则返回left
return left;
}else if(array[mid]>array[right]){
//如果left下标的值小于right下标的值,同时mid下标的值大于right下标的值
//例如 array[left]=4 array[right]=9 array[mid]=20
// 4 9 20 则返回right
return right;
}else{
return mid;
}
}
}
private static int partition(int[] array, int start, int end){
//选最左元素为基准值,挖第一个坑
int temp=array[start];
//循环填坑,直到start和end相遇
while (start<end){
//从右向左找<基准值的元素,填左坑
while (start<end&&array[end]>=temp){
end--;
}
//找到小于基准值的元素,填入start坑
array[start]=array[end];
//从左向右找>基准值的元素,填右坑
while (start<end&&array[start]<=temp){
start++;
}
//找到大于7的元素,填入end坑
array[end]=array[start];
}
//指针相遇,将基准值填入最后一个坑
array[start]=temp;
//返回基准值的最终位置
return start;
}
经过上面的不断优化,我们可以认为,我们的时间复杂度是O(N*logN)
v.非递归实现快速排序
我们可以通过栈来实现非递归的快速排序
每次都将数组的待排序区间的左指针和右指针进行入栈,然后进行出栈,出栈后调用分区方法,分区方法完成后,再分别将待排序的左指针和右指针进行入栈,直到栈空了才停止,一旦栈空了,说明数组中的数据也就有序了
java
public static void quickSort(int[] array){
quickNor(array,0,array.length-1);
}
private static void quickNor(int[] array, int left, int right) {
//用于存储待排序区间的「左边界、右边界」
Stack<Integer> stack=new Stack<>();
int mid=findMidNum(array,left,right);
swap(array,mid,left);
//对初始区间[left, right]执行分区操作,返回基准值最终位置pivot
int pivot=partition(array,left,right);
//条件pivot-left>1 → 等价于left < pivot-1,说明区间长度>1(有效,需要排序)
if (pivot-left>1){
//先存左边界left,后存右边界pivot-1
//同时出栈的时候,也是右边界先出
stack.push(left);
stack.push(pivot-1);
}
//条件right-pivot>1 → 等价于pivot+1 < right,说明区间长度>1(有效)
if (right-pivot>1){
//先存左边界pivot+1,后存右边界right
stack.push(pivot+1);
stack.push(right);
}
//循环处理栈中的所有待排序区间,直到栈空
while (!stack.isEmpty()){
//先取栈顶的「右边界end」(后入栈的),再取「左边界start」
int end=stack.pop();
int start=stack.pop();
mid=findMidNum(array,start,end);
swap(array,mid,start);
//对当前区间[start, end]执行分区,得到新的基准值位置pivot
pivot=partition(array,start,end);
//处理当前区间的左子区间[start, pivot-1],有效则入栈
if (pivot-start>1){
stack.push(start);
stack.push(pivot-1);
}
//处理当前区间的右子区间[pivot+1, end],有效则入栈
if (end-pivot>1){
stack.push(pivot+1);
stack.push(end);
}
}
}
4.归并排序
(1).基本思想
归并排序( MERGE-SORT )是建立在归并操作上的一种有效的排序算法 , 该算法是采用分治法( Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

(2).归并排序
首先我们要先将数组进行分解,分解完成后,才开始合并

java
public static void mergeSort(int[] array){
mergeSortTemp(array,0,array.length-1);
}
private static void mergeSortTemp(int[] array,int left,int right) {
if (left>=right){
return;
}
//划分区间,去中间下标,将[left,right]拆分为[left,mid],[mid+1,right]
int mid=(left+right)/2;
//递归排序左子区间
mergeSortTemp(array,left,mid);
//递归排序右子区间
mergeSortTemp(array,mid+1,right);
//当递归结束后,开始合并
//合并两个有序子区间,得到更大的有序区间
merge(array,left,mid,right);
}
private static void merge(int[] array,int left,int mid,int right) {
//创建临时数组,长度=待合并区间的长度,用来存储合并后的有序结果
int[] temp=new int[right-left+1];
int k=0; //临时数组的下标指针
int s1=left;//左子数组的起始指针
int e1=mid; //左子数组的结束指针
int s2=mid+1;//右子数组的起始指针
int e2=right;//右子数组的结束指针
//双指针遍历两个有序的子数组,按从小到大顺序填入临时数组
while (s1<=e1&&s2<=e2){
//左子数组当前元素更小,放入到临时数组
if (array[s1]<=array[s2]){
temp[k++]=array[s1++];
}else{
//右子数组当前元素更小,放入临时数组
temp[k++]=array[s2++];
}
}
//处理左子数组的剩余元素
while (s1<=e1){
temp[k++]=array[s1++];
}
//处理右子数组的剩余元素
while (s2<=e2){
temp[k++]=array[s2++];
}
//将临时数组的有序结果,复制回元素组的【left,right】区间
for (int i = 0; i < k; i++) {
array[left+i]=temp[i];
}
}
特性总结:
时间复杂度:O(NlogN)
可以看出来,归并排序的分解过程,为递归的深度所以是O(logN),合并的时
候,每次的N个元素都需要进行遍历,所以为O(N),所以时间复杂度就是
O(NlogN)。
空间复杂度:O(N)
在合并的时候,需要开辟辅助数组,所以空间复杂度是O(N)
稳定性:稳定的排序
(3).非递归实现归并排序
非递归实现归并排序,我们只需要改变归并排序中的分解过程即可,
我们可以回看我们递归实现的归并排序的部分代码
java
//划分区间,去中间下标,将[left,right]拆分为[left,mid],[mid+1,right]
int mid=(left+right)/2;
//递归排序左子区间
mergeSortTemp(array,left,mid);
//递归排序右子区间
mergeSortTemp(array,mid+1,right);
可以看到,我们是通过取中间的数进行的分组,那么我们改成非递归的时候,我们可以这样想,一开始,可以假设每个数都有序,当一个一个的数都有序的时候,在排序的过程中便可以变成2个,2个有序,2个,2个有序之后,可以变成4个,4个有序,··········直到一组数据全有序
java
public static void mergeSort(int[] array){
// mergeSortTemp(array,0,array.length-1);
mergeSortNor(array);
}
//归并排序非递归版本是放弃递归的拆分
//以步长gap为分组依据,从 单个元素 开始,步长倍增,相邻合并,把两个有序的小区间
//合并为一个有序的大区间,逐层向上整合,最终得到完整的有序数组
private static void mergeSortNor(int[] array) {
//gap表示当前带合并的单个有序区间的长度
int gap=1;
while (gap<array.length){
for (int i = 0; i < array.length; i+=gap*2) {
//定义第一个有序区间的边界:[start,mid]
int start=i;
//mid表示第一段的结尾
int mid=start+gap-1;
if (mid>=array.length){
mid=array.length-1;
}
//定义第二个有序区间的边界:[mid+1,end]
int end=mid+gap;
if (end>=array.length){
end=array.length-1;
}
//找到两个相邻的两个有序的区间,调用merge方法合并为一个更大的有序区间
merge(array,start,mid,end);
}
//外层控制步长,内层遍历数组的时候,下标i每次前进
// gap*2---刚好跳过,已经合并完成的两个区间,定位到下一组待合并区间的起始位置
gap*=2;
}
}
(三).七大排序的总结
|--------|----------------|---------|-----|
| 排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
| 直接插入排序 | O(N^2) | O(1) | 稳定 |
| 希尔排序 | O(N^1.3~1.5) | O(1) | 不稳定 |
| 直接选择排序 | O(N^2) | O(1) | 不稳定 |
| 堆排序 | O(N*logN) | O(1) | 不稳定 |
| 冒泡排序 | O(N^2) | O(1) | 稳定 |
| 快速排序 | O(N*logN) | O(logN) | 不稳定 |
| 归并排序 | O(N*logN) | O(N) | 稳定 |
(四).计数排序
计数排序是不需要数据与数据之间进行比较的排序,计数排序hi利用下标来表示原数组的具体的数据,用来表示原数组中每个元素出现的次数。根据统计的结果,按顺序重新填充原数组,实现排序。
计数数组要开的空间数:
我们可以在原数组中找出最大值和最小值,然后用 最大值-最小值+1,这个值就是我们计数数组要开辟的空间数
可以看到,60出现了两次,所以计数数组下标为0的位置的值就是2,61出现了2次,所以计数数组下标1的位置就是2,以此类推········
在放的时候我们可以这样放countArr[ array[i] - minValue ]
由此也可以看出来,计数排序时使用场景是:待排序的数据集中在某一个范围内,数据的取值范围不大。
如何从计数数组中还原已经排列好的数据?
我们可以通过计数数组的下标+minValue来得到真实的数据
java
public static void countSort(int[] array){
//首先,先遍历数组,找出数组中的最大值和最小值
int minValue=array[0];
int maxValue=array[0];
for (int i = 1; i < array.length; i++) {
if (array[i]>maxValue){
maxValue=array[i];
}
if (array[i]<minValue){
minValue=array[i];
}
}
//创建计数数组
int len=maxValue-minValue+1;
int[] countArray=new int[len];
//统计原数组中每个数值的出现次数(映射到计数数组下标)
for (int i = 0; i < array.length; i++) {
//array[i]-minValue ->压缩数组的长度
countArray[array[i]-minValue]++;
}
//根据计数数组,重新填充原数组
int index=0;//原数组的填充指针
for (int i = 0; i < countArray.length; i++) {
//遍历计数数组下标,while循环处理重复数值
while (countArray[i]!=0){
array[index]=i+minValue;//下标映射回原数值
index++;
countArray[i]--;
}
}
}
特性总结:
时间复杂度:O(N+范围)
我们刚开始找最大值和最小值的时候,时间复杂度是O(N),在统计原数组中
每个数值出现的次数的时候,时间复杂度是O(N),在重新填充原数组的时
候,时间复杂度是我们申请的数组的长度,也就是最大值和最小值的差的范
围,所以总结来说,时间复杂度就是O(2*N+范围)
空间复杂度:O(范围)
在进行重新填充原数组的时候,我们申请了新的计数数组,而这个计数数组
的大小也是根据原数组中最大值和最小值的范围来算的
稳定性:稳定的排序








