文本目录:
❄️一、交换排序:
➷ 1、 冒泡排序:
▶ 代码:
➷ 2、 快速排序:
☞ 基本思想:
☞ 方法一:Hoare法
▶ 代码:
☞ 方法二:挖坑法
▶ 代码:
☞ 方法三:前后指针法
▶ 代码:
☞ 优化:
☑ 方法一:三数取中法
▶ 优化后的代码:
☑ 方法二:递归到一定小的区间时,进行插入排序
▶ 优化后的代码:
☞ 非递归实现快速排序:
▶ 代码:
❄️总结:
❄️一、交换排序:
对于交换排序的思想就是:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。
交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动
➷ 1、 冒泡排序:
对于这个排序方法就是我们很熟悉的一种排序了,我们的在 C语言中也使用过这种排序,在这里呢我们呢就简单的看一遍代码就可以了。来看看最优化的代码:
▶ 代码:
/**
* 冒泡排序
* 时间复杂度:这里需要分情况:
* 没有优化:O(N^2) 优化后:最好可以达到O(N)
* 空间复杂度:O(1)
* 稳定性:稳定
* @param array
*/
public static void bubbleSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
boolean flag = true;
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
swap(array,j,j + 1);
flag = false;
}
}
if (flag) {
break;
}
}
}
这个呢就是我们的优化后的冒泡排序了。
➷ 2、 快速排序:
快速排序是 Hoare 提出的一种二叉树结构的排序方法。
☞ 基本思想:
任取待排序元素序列中的某个元素作为基准值,按照该排序码将其待排序集合分割成两个序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后所有左右序列重复该过程,直到所有元素都排在相应的位置上为止。
我们对于快速排序呢有三种做法,我们一个一个来看:
☞ 方法一:Hoare法
对于这个方法呢,我们就是在最开始的数组中,
0 下标位置设一个left,在最后一个下标设一个right,我们的把 left 这个下标的值 作为基准值
之后我们从后面开始找比 基准值 小的值停下来,从前面开始找比 基准值 大的值停下来,
之后交换我们的 left 和 right 的值,之后继续走,直到left和right相遇停止,之后把 left 下标的值和我们的基准值进行交换。
这样呢,我们的 基准值 的左面都比基准值小,基准值 的右面都比基准值大。
之后我们再把基准值左面的和右面的再次执行上述的操作,直至剩下一个数据就有序了。
我们来看看这个方法的流程图:
OK,这就是我们的这个方法的流程,这个呢是比较难理解的,要多看几遍如果不是很理解的话。
但是在上面图中,我们要注意,我们的开始和结束为止呢,不能使用 left 和 right 的,我们的left和right 就是被赋值的形参,我们的实参使用 start 和 end来表示 开始与结束位置。
▶ 代码:
我们来看看这个方法一的代码:
/**
* 快速排序的 Hoare 方法
*
* 时间复杂度:
* 最坏的情况下:当数据为1 2 3 4 5 6 7 8 9 10 ......或者
* .......10 9 8 7 6 5 4 3 2 1 时候呢为 O(N^2)
* 最好的情况下:O(N*logN)
* 我们使用快排呢都是 O(N*logN)
* 空间复杂度:
* 最坏的情况下:O(N)
* 都在一面,我们的递归需要开辟空间
*
* 最好的情况下:O(logN)
* 稳定性:
* 不稳定的
* @param array
*/
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 tmp = array[left];
int tmpleft = left;//把一开始的left位置保存下来,方便我们后面进行交换
while (left < right) {
while (left < right && array[right] >= tmp) {
right--;
}
while (left < right && array[left] <= tmp) {
left++;
}
//两个if语句判断结束之后呢,我们的 right 是比 tmp 小的
//left 是比 tmp 大的
//进行交换
swap(array,left,right);
}
//循环结束,我们的left和right相遇,把基准值交换
swap(array,left,tmpleft);
//这时候我们的 left 就是我们的 基准值 交换后的下标
return left;
}
这里要注意我们要从后往前开始找,不能从前往后,因为可能会和大的值进行交换
这样就把 7 交换到 前面去了,不可以。
而且我们的从后往前找和从前往后找的时候一定要有等于号,如果没有就会出现死循环的情况: 这个呢就是我们快速排序的 Hoare 方法了。我们来看看下一种方法:
☞ 方法二:挖坑法
挖坑法就是我们一开始把我们 left 位置设置的基准值 拿出来放到 pivot 变量中,
我们之后 从后往前走 right 找到比 pivot 小的元素 放到我们的 left 中(因为我们一开始把 left 的基准值拿出来了,所以相当于里面没有元素),放完之后我们的 right 停下,之后再移动我们的 left 从前面找比 pivot 的值大的元素,放到我们的 right 中,
之后循环这个过程,直至我们的 right 和 left 相遇,这时把 pivot 放到 left 中,这样就实现了我们的基准值的左面都比其小,右面都比其大。
之后再在基准值的左面和右面都执行这个操作,直至剩余一个元素。
我们呢可以发现啊,对于这个挖坑法呢和上面我们介绍的 Hoare 的方法还是很相似的。我们来看看这个挖坑法的流程图:
这个呢就是我们挖坑法的流程图了,虽然我没有画的完全,但是呢还是可以理解的,我们呢来看代码如何编写:
▶ 代码:
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 tmp = array[left];//这个就是基准值
while (left < right) {
while (left < right && array[right] >= tmp) {
right--;
}
//找到比 基准值小的放到 left中
array[left] = array[right];
while (left < right && array[left] <= tmp) {
left++;
}
//找到比 基准值大的放到 right中
array[right] = array[left];
}
//right和left相遇后,把 tmp 放到left中
array[left] = tmp;
return left;
}
我们一般以挖坑法为主,挖坑法是三种方法最重要的,如果题中没有要求使用哪种方法,我们默认使用挖坑法。
这个方法就结束了,我们来看看下一种方法:前后指针法。
☞ 方法三:前后指针法
对于这个方法就是,在 left 的位置设置一个 prev 在 left +1 的位置设置一个 cur ,
之后我们判断 cur 的元素是否小于 left 的元素(基准值),并且 prev 的下一个元素是否等于 cur 的元素。
如果 cur 的元素小于left 的元素的值并且 prev 的下一个元素不等于 cur 的元素,就把cur 和prev 的元素进行交换。反之,则 cur++。
一直循环这个操作,直至 cur 大于了right 这个位置。这个时候,我们把 prev的值和left 的值进行交换。
就实现了 基准值的左面都是小于基准值的,右面都是大于基准值的。
我们来看流程图:
这个呢就是我们的呢前后指针的流程图了,我虽然没有在上图中把遍历左面和右面的流程画出来,但是呢和我们图中的执行流程是差不多的,我呢就不在这里画了,我们来直接看代码:
▶ 代码:
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 = partition2(array,start,end);//找到基准值并交换到一定位置,这时候就把待排序的数据分成了左右两份
quick(array,start,pivot - 1);//排序基准值左面的
quick(array,pivot + 1,end);//排序基准值右面的
//左右排完就是有序的了
}
//前后指针法
private static int partition2(int[] array,int left,int right) {
int prev = left;
int cur = left + 1;
while (cur <= right) {
if (array[cur] < array[left] && array[++prev] != array[cur]) {
swap(array,cur,prev);
}
cur++;
}
swap(array,prev,left);
return prev;
}
这个时候我们返回的是 prev 而非 left,这里需要注意。
☞ 优化:
当我们使用快排的时候可能会出现这种情况:
我们有两种优化方式:
☑ 方法一:三数取中法
我们的这个是什么意思呢?我们上面的数据为例,就是找到 left 和 right 的中位数 mid 这时候呢,我们从 left 和 right 和 mid 中找到一个中位数,和我们的的开始位置进行交换,就是我们的基准值了。我们来看看例子:
这就是我们 三数取中法, 这样是不是就不是单分支的了,就可以变成多分支的了。
那么我们要如何才能找到我们的中间值呢?我们有两种情况:
第一种情况:array[left] < array[right] 的时候。
第二种情况:array[left] > array[right] 的时候。
简单来说就是 找中间值 和 开始的位置进行交换,之后再执行我们的 挖坑法 来进行快速排序。
▶ 优化后的代码:
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 mid = getMiddleNum(array,start,end);
swap(array,start,mid);
int pivot = partition(array,start,end);//找到基准值并交换到一定位置,这时候就把待排序的数据分成了左右两份
quick(array,start,pivot - 1);//排序基准值左面的
quick(array,pivot + 1,end);//排序基准值右面的
//左右排完就是有序的了
}
//优化:三数取中法
private static int getMiddleNum(int[] array,int left,int right) {
int mid = (left + right) / 2;
if (array[left] < array[right]) {
if (array[mid] < array[left]) {
return left;
} else if (array[mid] > array[right]) {
return right;
}else {
return mid;
}
}else {
if (array[mid] > array[left]) {
return left;
} else if (array[mid] < array[right]) {
return right;
}else {
return mid;
}
}
}
这个就是我们的第一种优化方法。
☑ 方法二:递归到一定小的区间时,进行插入排序
我们来想,当我们对一组数据进行排序的时候呢,是不是 越排序数据越趋于有序的,在我们上个博客中介绍 直接插入排序的时候呢,也介绍了当数据越有序效率越高,所以我们可以在我们递归到一定小的区间时候呢,我们使用直接插入排序,来减少递归的次数,减少内存的开销。
我们的思路就是这样的,我们来看看代码是如何编写的,不能直接使用 直接插入排序,因为我们有区间,不是对整个数据进行 直接插入排序。
上一篇博客的传送门:
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;
}
//优化:直接插入排序
//当快排到一定的区间的时候我们的使用直接插入排序,来减少内存的开销
//这里的区间是自定义的
if (end - start +1 <= 8) {
insetSortRange(array,start,end);
return;
}
//优化:三数取中法
int mid = getMiddleNum(array,start,end);
swap(array,start,mid);
int pivot = partition(array,start,end);//找到基准值并交换到一定位置,这时候就把待排序的数据分成了左右两份
quick(array,start,pivot - 1);//排序基准值左面的
quick(array,pivot + 1,end);//排序基准值右面的
//左右排完就是有序的了
}
//优化:直接插入排序
private static void insetSortRange(int[] array,int start,int end) {
for (int i = start + 1; i <= end; i++) {
int tmp = array[i];
int j = i - 1;
for (; j >= 0; j--) {
if (array[j] > tmp) {
array[j + 1] = array[j];
}else {
array[j + 1] = tmp;
break;
}
}
//这是当我们的 j < 0的时候呢,
//我们退出循环之后相当于 j+1 为0下标
array[j + 1] = tmp;
}
}
这时候的整体的代码就是我们的最优状态了。
☞ 非递归实现快速排序:
对于 快速排序 的非递归方法呢,我们需要借用 栈 来完成,我们的步骤就是:
1、先把数据进行一次 挖坑法 排序,这时候呢我们就有了 pivot 这个位置。
2、判断 pivot 的左右是否是存在 2个或 2个以上的元素,判断方法:
如果 :pivot > start + 1 左面就有 2个或 2个以上的元素
如果: pivot < end - 1 右面就有 2个或 2个以上的元素
3、我们把 start 这个小标 、pivot - 1 这个下标、pivot + 1 这个下标 和 end 这个下标按顺序进行 入栈操作。
4、我们出栈顶数据先给 end 再给 start ,这时候使用出栈的 start 和 end 值进行 挖坑法排序
5、排完序后,再执行 操作2 和 操作3,之后是操作4,直至栈为空。就排好序了。
我们来看看代码如何编写的,理解之后呢就非常的简单了:
▶ 代码:
public static void quickSort(int[] array) {
quickNor(array,0,array.length - 1);
}
//快速排序的非递归实现
private static void quickNor(int[] array,int start,int end) {
Stack<Integer> stack = new Stack<>();
//求第一次挖坑法之后的 基准值位置
int pivot = partition(array,start,end);
//我们的把start、pivot - 1 、pivot + 1 、end 都入栈
//我们还需要判断 基准值的左面和右面是否是大于等于2个的元素
if (pivot > start + 1) {
stack.push(start);
stack.push(pivot - 1);
}
if (pivot < end - 1) {
stack.push(pivot + 1);
stack.push(end);
}
while (!stack.isEmpty()) {
//出两个栈顶,先给end 后给start
end = stack.pop();
start = stack.pop();
pivot = partition(array,start,end);
//再次求完pivot 之后呢,在执行入栈操作
if (pivot > start + 1) {
stack.push(start);
stack.push(pivot - 1);
}
if (pivot < end - 1) {
stack.push(pivot + 1);
stack.push(end);
}
}
}
//挖坑法
private static int partition(int[] array,int left,int right) {
int tmp = array[left];//这个就是基准值
while (left < right) {
while (left < right && array[right] >= tmp) {
right--;
}
//找到比 基准值小的放到 left中
array[left] = array[right];
while (left < right && array[left] <= tmp) {
left++;
}
//找到比 基准值大的放到 right中
array[right] = array[left];
}
//right和left相遇后,把 tmp 放到left中
array[left] = tmp;
return left;
}
到这里我们的交换排序就结束了。
❄️总结:
OK,我们这次的博客就到这里就结束了,我们这次博客虽然我们只介绍了 一类排序——交换排序,但是呢我们这篇博客是非常重要的,因为我们的快速排序是经常使用的排序方法,所以要多加理解这个排序。
我们在下一篇博客就会把排序收尾,让我们尽情期待吧!!!拜拜~~~