排序算法
1.冒泡排序
原理:
从首个元素开始,反复依次对比相邻的两个元素,如果与预期的顺序(升序/降序)不符,则换位。
规律:
- 每1轮可以确定1个数字的在正确的位置,在反复执行的过程中依次确定右侧第1个、第2个、第3个…位置的数字
- 循环的次数 = 数组的长度 - 1
- 每轮循环需要对比的元素个数递减,每轮循环将最大的元素移到右侧,后续循环不必再对该数字做对比排序操作
代码解析:
- 外层循环表示循环的轮次,数组的长度 - 1
- 初始条件: int i = 0
- 循环条件: i < array.length - 1
- 内层循环用于对比和换位
- 循环条件:数组长度 - 当前轮次 - 1
- 当前轮次使用0作为初始值来计数
public class BubbleSort {
public static void main(String[] args) {
int[] array = {8,1,4,9,0,3,5,2,7,6};
System.out.println(Arrays.toString(array));
System.out.println();
for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - 1 - i ; j++) {
if(array[j] > array[j + 1]){
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
System.out.println(Arrays.toString(array));
}
}
2.选择排序
原理:
将未排序的第一个数字和剩余的每个数字进行对比,如果与预期的顺序(升序/降序)不符,则换位。
规律:
- 每1轮可以确定1个数字的在正确的位置,在反复执行的过程中依次确定左侧第1个、第2个、第3个…位置的数字
- 循环的次数 = 数组的长度 - 1
- 每轮循环需要对比的元素个数递减,每轮循环将最小的元素移到左侧,后续循环不必再对该数字做对比排序操作
代码解析:
-
外层循环表示循环的轮次,数组的长度 - 1
- 初始条件: int i = 0
- 循环条件: i < array.length - 1
-
内层循环用于对比和换位
-
初始条件: int j = i + 1
-
循环条件:j < array.length
-
public class SelectionSort {
public static void main(String[] args) {
int[] array = {8,1,4,9,0,3,5,2,7,6};
System.out.println(Arrays.toString(array));
System.out.println();
for (int i = 0; i < array.length - 1; i++) {
for (int j = i + 1; j < array.length; j++) {
if (array[i] > array[j]){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
}
System.out.println(Arrays.toString(array));
}
}
3.插入排序
原理:
-
假定第1个数字是最小的(升序排列),从第2个数字开始,依次将数字插入到它应该放置的位置(通过倒着对比交换执行),类似玩扑克牌时整理手上的扑克牌。
-
暂未参与排序的数字,可以视为玩扑克牌时仍放在桌上未抓取的扑克牌。
(即从左往右不断扩大排序的区域)
规律:
- 每一轮当前元素左侧的元素都是有序的,当当前元素与左侧元素无需换位时,无需再与更左侧元素进行对比。
效率:
随着排序的进行需要对比和换位的操作均可能明显减少,所以,插入排序的效率明显高于冒泡排序和选择排序!
代码解析:
-
外层循环表示循环的轮次,i 表示此轮加入排序区域的元素下标
- 初始条件: int i = 1
- 循环条件: i < array.length
-
内层循环用于对比和换位
-
初始条件: int j = i
-
while循环条件:j > 0 且满足 array[j] < array[j - 1]
-
public class InsertionSort {
public static void main(String[] args) {
int[] array = {8,1,4,9,0,3,5,2,7,6};
System.out.println(Arrays.toString(array));
System.out.println();
for (int i = 1; i < array.length; i++) {
int j = i;
while (j > 0){
if (array[j] < array[j - 1]){//判断当前元素是否比左侧元素小,是则换位
int temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
j--;
}else {//不满足条件时跳出循环,无论左侧还有多少元素,都应比j位置的元素小
break;
}
}
}
System.out.println(Arrays.toString(array));
}
}
4.希尔排序
原理:
通过相距一定间隔的元素来工作,各轮对比所用的距离随着算法的进行而减小,走到只比较相邻元素的最后一轮排序为止。所以也叫缩减增量排序。
(外加一层循环的执行多次不同间隔的插入排序,间隔为1时是一个普通的插入排序)
规律:
-
每轮间隔大小减半
-
第三轮(同插入排序第二轮):
每一轮当前元素左侧同间隔的元素都是有序的,当当前元素与左侧元素无需换位时,无需再与更左侧元素进行对比。
代码解析:
-
外层循环表示不同间隔插入排序的轮次
- 初始条件: 增量(gap)值为数组长度除以2
- 循环条件: 增量 > 0
- 条件自变: 增量自除2
-
中层循环表示该次插入排序中遍历的轮次,i 表示此轮加入排序区域的元素下标
- 初始条件: int i = gap
- 循环条件: i < array.length
-
内层循环用于对比和换位,j 表示被对比元素的下标
-
初始条件: int j = i - gap
-
循环条件: j >= 0 且满足 array[j] > array[j + gap]
-
public class ShellSort {
public static void main(String[] args) {
int[] array = {8,1,4,9,0,3,5,2,7,6};
System.out.println(Arrays.toString(array));
//多轮循环: 缩减增量
//初始条件: 增量(gap)值为数组长度除以2
//循环条件: 增量 > 0
//条件自变: 增量自除2
for (int gap = array.length/2; gap > 0; gap /= 2) {
//从与增量值相等大小的下标位置开始,向右循环
for (int i = gap; i < array.length; i++) {
//接下来的循环表示向左找”同组“的元素尝试对比及必要的排序
//j: 左侧同组元素的下标,即被对比元素的下标
for (int j = i - gap; j >= 0; j -= gap) {
if (array[j] > array[j + gap]){
int temp = array[j];
array[j] = array[j + gap];
array[j + gap] = temp;
}else {
break;
}
}
}
}
System.out.println(Arrays.toString(array));
}
}
5.归并排序
原理:
-
核心思想:将两个整体无序**(但自身有序!!!)**的”小”数组的所有元素有序地填充到一个新的数组中去,排序地同时形成合并。
- 先将2个数组的第1个元素进行对比,将较小的数字(升序排列)填充到新数组中。
- 将取走数据的数组的下一位,与未被取走数据的数组的那一位对比:
- 继续相同操作…
- 直到最后一个放进去
-
事实上,在实际应用中执行排序时,需要排序的原数组只有一个,并且长度通常超过2,那么,就需要先将大数组进行反复拆分,得到如同以上的小数组(执行时会拆至数组长度为1),再进行合并操作,这是一种经典的分治策略(将问题分成小问题,然后递归求解)。
递归:表现为在方法的内部,调用当前方法本身
- 例如再a()方法内部调用a()方法
- 仅当内部调用的a()方法运行结束,才会继续运行外部a()方法的剩余代码
- 在不熟练的情况下,不要轻易使用递归,否则将形成一种“死循环”的效果
规律:
实际上每轮递归后又能保证返回给上一轮的“小”数组是有序的,直至递归结束,我们可以得到一个有序数组。
代码解析:
- 递归拆分数组
- 取数组长度的中值将数组对半拆分:mid = start + (end - start)/2
- 防止递归“死循环”
- 当end与start相同时(发生在尝试对长度为1的数组再拆分时),不必再执行递归
- 将两个小数组合并为有序的大数组
public class MergeSort {
public static void main(String[] args) {
int[] array = {8,1,4,9,0,3,5,2,7,6};
System.out.println(Arrays.toString(array));
System.out.println();
int[] newArray = MergeSort(array,0,array.length-1);
System.out.println(Arrays.toString(newArray));
}
public static int[] MergeSort(int[] array, int start, int end){
//递归停止的条件!!!
if (start == end){//拆分至数组长度为1时
return new int[]{array[start]};
}
//将数组对半拆分
int mid = start + (end - start)/2;//数组下标的中值
int[] leftArray = MergeSort(array, start, mid);
int[] rightArray = MergeSort(array, mid+1, end);
int[] newArray = new int[leftArray.length + rightArray.length];
int l = 0;//左半部分数组下标
int r = 0;//右半部分数组下标
int n = 0;//新数组下标
while (l < leftArray.length && r < rightArray.length){//当左右都有未装入新数组的元素时
//三目:对比左右数组当前元素,将较小的值(升序)装入新数组
newArray[n++] = leftArray[l] < rightArray[r] ? leftArray[l++] : rightArray[r++];
}
while (l < leftArray.length){//仅左半数组有元素未装入新数组时
newArray[n++] = leftArray[l++];
}
while (r < rightArray.length){//仅右半数组有元素未装入新数组时
newArray[n++] = rightArray[r++];
}
return newArray;//返回有序的新数组
}
}
6.快速排序(一)
原理:
-
先挑选数组中的某个元素,它将作为所有元素排列大小的分界值
-
作为分界值的数组元素称之为:枢纽元(pivot)/主元
-
(升序排序)需要将比枢纽元小的元素放在其左侧位置,将比枢纽元大的元素放在其右侧位置。并不关心其左侧或右侧区域的元素是否有序。
-
以这个数组为例:
-
可以使用最右侧的6作为枢纽原,如需升序排列,则需要先达到以下效果:
(暂时使用最右侧元素作为枢纽元)
-
将原数组根据枢纽元划分开来的过程称之为:分区
- 你需要再脑海中将数组的最终目标想象成下图的几个区域组成
- 注意:在分区结束之前,枢纽元位置的元素不会发生换位操作
- 将“<=枢纽元”的区域称之为“左侧分区”,将“>枢纽元”的区域称之为“右侧分区”
- 第一次分区的过程大概是这样:先确定枢纽元
- 使用第1个数字与枢纽元比较大小
- 再使用数组的第二个元素与枢纽元进行对比
- 由于1小于6,所以,1也在左侧分区,并且不用换位
- 尽管第2位的1比第1位的5更小,但不需要换位,后续会使用递归再次处理
- 再使用数组的第3个元素与枢纽元进行的对比
- 由于9大于6,所以,9在右侧分区
- 不需要换位,只需标记位置即可,此时枢纽元元素仍在数组的最右侧
- 再使用数组的第4个元素与枢纽元进行对比
- 由于3小于6,所以3在左侧分区
- 由于各分区是必须是连续的,所以此次需要换位
- 再使用数组的第5个元素与枢纽元进行对比
- 再使用数组的第6个元素与枢纽元进行对比
- 由于2小于6,所以2在左侧分区,需要换位,它只需要与右侧分区的第一个元素换位即可,在右侧分区的其他元素不需要调整
- 再使用数组的第7个元素与枢纽元进行对比
- 全部完成后,将枢纽元与右侧分区的第1个元素换位
- 将得到第1次分区的最终结果
规律:
实际上快速排序始终对同一数组的片段进行处理,未拆分,则无需合并。
代码解析:
- 快速排序本质上没有拆出新数组,也不需要合并,所以方法不需要返回值(void)
- 使用1个变量x,表示左侧区域的最后一个元素
- 默认值是当前数组区域的第1个元素的左侧
- 如果是原数组左侧第1个元素开始处理,可以使用-1
- 在递归过程中,处理的可能是数组的中间某个片段,则是该片段最左侧下标 - 1
- 遍历数组的片段,将该区域范围的元素遍历即可(start -> end)
- 当前元素 <= pivot :
- 尚未出现 > pivot 的元素,则更新 x 的值为当前遍历的下标(i)
- 已经出现 > pivot 的元素(i - x > 1),需换位,更新 x 的值(自增)
- 当前元素 > pivot : 无需操作,继续循环即可
- 交换枢纽元与第1个大于枢纽元的元素的位置,即:将枢纽元放在左右区域中间
- 判断是否需要递归:
- 左侧区域仍有多个元素,则递归处理
- 右侧区域仍有多个元素,则递归处理
public class QuickSort {
public static void main(String[] args) {
int[] array = {8,1,4,9,0,3,5,2,7,6};
System.out.println(Arrays.toString(array));
System.out.println();
quickSort(array,0,array.length-1);
System.out.println(Arrays.toString(array));
}
public static void quickSort( int[] array, int start, int end){
//选择最右侧元素的值做为枢纽元
int pivot = array[end];
//标记左侧区域的位置,在需遍历的数组片段的左侧
int x = start - 1;
//遍历数组的片段,将该区域范围的元素遍历即可(start -> end)
for (int i = start; i < end; i++) {
if(array[i] <= pivot){//判断当前元素是否小于枢纽元
if(i - x > 1){//判断当前元素左侧是否已存在大于枢纽元的元素
int temp = array[i];
array[i] = array[x+1];
array[x+1] = temp;
x++;
}else {
x = i;
}
}
}
//交换枢纽元与第1个大于枢纽元的元素的位置,即:将枢纽元放在左右区域中间
if (array[x+1] > pivot){
array[end] = array[x+1];
array[x+1] = pivot;
}
//左侧区域仍有多个元素,则递归处理
//因为x是左侧区域的最右侧元素下标
//若比需要处理的数组片段的最左侧元素下标大,则表示左侧存在超过1个元素
if (x > start){
quickSort(array,start,x);
}
//若左侧区域仍有多个元素,则递归处理
//x是左侧区域的最右侧元素下标,x+1为枢纽元位置
if(end - x - 1 > 1 ){
quickSort(array,x+2,end);
}
}
}
7.快速排序(二)
原理:
-
第一步仍是确认枢纽元,此次任然使用最右侧元素作为枢纽元
- 也可以是最左侧元素,这并不影响
- 假设x是左侧区域的最大下标,y是右侧区域的最小下标
- 默认情况下,x在最左侧,y在元素枢纽元元素的位置,会通过循环来移动下标位置
- 当满足y > x 时,(y位置元素开始)从右至左判断每个元素,只要元素值大于枢纽元,y 值自减
- 可以理解为从右制检索小于枢纽元的元素
- 显然,枢纽元自身就不符合“小于枢纽元”的条件,则 y 不变,且停止移动
- 当满足 y > x 时,从左至右判断每个元素,只要元素值小于枢纽元,x 值自增
- 可以理解为从左至右检索大于枢纽元的元素
- 提示:在本例中,最左测第一个元素满足“大于枢纽元”的条件,则 x 不变
- 此时需要换位
- 得到的结果时这样的
- 循环此前的操作,继续从右至左检索,当 y 所指向的元素不满足“大于枢纽元”的条件时停止
-
再从左至右检索,由于指向的元素变成了枢纽元,依然不满足“小于枢纽元”的条件,则不会移动,x 并没有变化
-
得到的结果时这样的
- 继续循环此前的操作,继续从右至左检索,当然,由于 y 指向的元素又变成枢纽元了,仍不移动
- 再从左至右检索,当 x 所指向的元素不满足“小于枢纽元”的条件时停止
- 然后,继续换位,一直重复类似的操作,各步的变化如下:
- 最后,将会不在满足x < y 的条件,则此轮分区结束
- 存在比较特殊的情况:数组中有多个与枢纽元大小相同的元素
- 将 x 自增,以跳过这个元素,否则就会”卡死“
- 接下来,通过递归即可完成排序
- 当 x - 1 > start时,表示左侧区域仍有元素,执行递归
- 当 y + 1 < end 时,表示右侧区域仍有元素,执行递归
规律:
同上,实际上快速排序(二)始终对同一数组的片段进行处理,未拆分,则无需合并。
代码解析:
- 快速排序本质上没有拆出新数组,也不需要合并,所以方法不需要返回值(void)
- 声明2个变量 x 和 y ,分别表示左侧区域的最大下标,右侧区域的最小下标
- x 默认值是当前数组区域的第1个元素的值,y 默认值时前数组区域的最后一个元素的值
- 遍历数组的片段,当x < y 时,说明还有未遍历的元素
- x 指向的元素 < pivot 且 x < y 时,x 向左移动
- y 指向的元素 > pivot 且 x < y 时,y 向右移动
- x与y指向的元素是否与枢纽元相同时,x 向左移动,否则卡死
- x 与 y 并不都指向枢纽元,此时x元素大于枢纽元或y元素小于枢纽元,需要换位
- 判断是否需要递归:
- 左侧区域仍有多个元素,则递归处理
- 右侧区域仍有多个元素,则递归处理
public class QuickSort2 {
public static int compareCount;
public static int swapCount;
public static void main(String[] args) {
int[] array = {8,1,4,9,0,3,5,2,7,6};
System.out.println(Arrays.toString(array));
System.out.println();
quickSort(array,0,array.length-1);
System.out.println(Arrays.toString(array));
}
public static void quickSort( int[] array, int start, int end){
//使用新的变量表示左侧区域的最大下标,右侧区域的最小下标
int x = start;
int y = end;
int pivot = array[end];//选择最后一个元素的值作为枢纽元
//当x在y的左侧时,说明还有需要检索的元素
while (x < y){
//从左至右找出小于枢纽元的元素,并标记位置,当指向的元素大于枢纽元时跳出
while (x < y && array[x] < pivot){//循环内x,y值会变化,仍需判断x<y
x++;
}
//从右至左找出大于枢纽元的元素,标记位置,当指向的元素小于枢纽元时跳出
while (x < y && array[y] > pivot){//循环内x,y值会变化,仍需判断x<y
y--;
}
//判断现在的x与y指向的元素是否与枢纽元相同
if (x < y && array[x] == array[y]){
x++;//若相同则x向右移动,否则卡死
}else {//x与y并不都指向枢纽元,此时x元素大于枢纽元或y元素小于枢纽元,需要换位
int temp = array[x];
array[x] = array[y];
array[y] = temp;
}
}
//如果左侧区域仍有多个元素,则递归
if (x - 1 > start){
quickSort(array,start,x-1);
}
//如果右侧区域仍有多个元素,则递归
if (y + 1 < end){
quickSort(array,y+1,end);
}
}
}
快速排序枢纽元的选择
1.为什么不选择两端的元素作为枢纽元
对于一个有序数组来说,使用最左或最右的元素的值作为枢纽元效率会明显比选择随机枢纽元低
2.随机枢纽元代码分析
生成当前数组区域的左右下标之间的随机数:
Random random = new Random();
int randomIndex = random.nextInt(end - start) + start;
int pivot = array[randomIndex];
3.选择位置合适的和枢纽元
4. 三数中值分割法
- 使用最左侧,最右侧,中心位置的三个元素的中值(中间大小的那个)作为枢纽元
- 可以消除有序数组的坏情况
//假设最右侧数据是最适合的枢纽元
int pivot = array[end];
//当数组片段的长度超过2时,才使用三数中值分割法
if (end - start > 2){
//定义三个变量,分别表示最左侧值,中值,最右侧值,使用新的变量主要是为了便于理解代码
int a = array[start], b = array[(end - start) / 2], c = array[end];
//判断哪个是中值:中值分别减去另2个数,必然得到一正(或0)一负(或0),正数和负数相乘结果小于等于零
//最大值分别减去另外2个数必然得到两个正数(或0),相乘结果大于或等于零
//最小值分别减去另外2个数必然得到两个负数(或0),相乘结果大于或等于零
if ((a - b) * (a - c) <= 0){
pivot = a;
}else if ((c - a) * (c - b) <= 0 ){
pivot = c
}else {
pivot = b;
}
}
8.排序算法的选取
效率
稳定性
int a = array[start], b = array[(end - start) / 2], c = array[end];
//判断哪个是中值:中值分别减去另2个数,必然得到一正(或0)一负(或0),正数和负数相乘结果小于等于零
//最大值分别减去另外2个数必然得到两个正数(或0),相乘结果大于或等于零
//最小值分别减去另外2个数必然得到两个负数(或0),相乘结果大于或等于零
if ((a - b) * (a - c) <= 0){
pivot = a;
}else if ((c - a) * (c - b) <= 0 ){
pivot = c
}else {
pivot = b;
}
}