Java中常用的数组排序算法
01-排序概述与数据换位
1.排序
- 被排序数据是一组相同类型、相同定位的数据,数组表示的就是这样的数据
- 核心:“对比"和"换位”
- 根据“对比”的标准不同,可产生“升序”和“降序”结果
- 在某些特殊的排序算法中可能没有“换位”操作,详见后续各算法原理
2.数据换位
-
如果只是让2个变量的值分别赋值给彼此,由于程序代码是依次执行的,一旦某个变量被赋予新的值,则其原来的值就会丢失!
-
为了保证在交换值的过程中,变量的原值不会丢失(被重新赋值导致的覆盖)﹐可以使用第3个变量临时存储变量的原值
-
也可以不使用临时变量,而使用算术运算来实现换位(代码语义较差)
02-冒泡排序(Bubble sort)
1.原理
-
反复对比相邻的两个元素,如果与预期的顺序不符,则换位
-
需要进行多轮循环,可以使用嵌套的循环来实现
-
外层循环表示循环轮次,数组的长度-1
初始条件: int i= 0
循环条件:i< array.length-1 -
内层循环用于对比和换位
循环条件:数组长度-当前轮次–1
当前轮次使用0作为初始值来计数
2.实现
package com.tt.test.demo1;
import java.util.Arrays;
import java.util.Random;
public class BubbleSort {
public static void main(String[] args){
//方式一:创建需要排序的数组对象
//int[] array = {8,1,4,9,0,3,5,2,7,6}
//方式二:使用随机数生成的数组
//随机数工具对象
Random random = new Random();
//设置生成的随机的数值大小上限(不含此值)
int numberBound = 100;
//设置生成随机数的数量,也是数组的长度
int numbersCount = 50;
//创建数组
int[] array = new int[numbersCount];
//遍历数组
for(int i = 0; i < array.length; i++){
//生成随机数,作为当前遍历到的数组元素的值
array[i] = random.nextInt(numberBound);
}
//输出显示生成的数组
System.out.println(Arrays.toString(array));
//记录排序之前的时间值
long startTime = System.currentTimeMillis();
//-------冒泡排序开始-------
//外层的循环
for(int i = 0; i< array.length - 1; i++){
//遍历数组
//循环变量j:某元素的下标,将始终与下标为j+1的元素进行对比
//注意:数组的最右侧元素没有对比对象,所以遍历至array.length-1即可
//注意∶内层循环的次数是递减的,需要改为j < 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));
}
// System.out.println();
}
//记录排序之后的时间值
long endTime = System.currentTimeMillis();
//输出排序后的数组
System.out.println(Arrays.toString(array));
//耗时
System.out.println("耗时" + (endTime - startTime) + "毫秒");
}
}
03-选择排序(Selection sort)
1.原理
- 将未排序的第1个数字和剩余的每个数字进行对比,如果与预期的顺序(升序)不符,则换位
- 需要进行多轮循环,可以使用嵌套的循环来实现
- 外层循环表示循环轮次,数组的长度- 1
- 初始条件: int i=0
- 循环条件:i< array.length - 1
- 内层循环用于对比和换位
- 初始条件: int j = i+1
- 循环条件: j<array.length
2.实现
package com.tt.test.demo1;
import java.util.Arrays;
public class SelectionSort{
public static void main(String[] args) {
//创建需要排序的数组对象
int[] array = {8,1,4,9,0,3,5,2,7,6};
//新的循环
for (int i = 0; i < array.length; i++) {
//遍历数组
//使用j表示被对比的元素下标
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));
}
System.out.println();
}
}
}
04-插入排序(Insertion sort)
1.原理
将第一个元素作为起始数组,不管这个元素是大还是小。然后用第二个元素与这个元素进行比较,如果比起始元素大,则放在起始元素之后,反之放在之前,组成一个有序集合,然后用其他元素跟有序集合中的元素比较,插入到正确的位置,以此类推达到有序。
2. 实现
package com.tt.test.demo1;
import java.util.Arrays;
public class InsertionSort {
public static void main(String[] args) {
//创建需要排序的数组对象
int[] array = {8,1,4,9,0,3,5,2,7,6};
//从下标为1的元素开始,反复对比左侧元素
for (int i = 1; i < array.length; i++) {
//当前需要确定位置的元素的下标,暂时假设是数组的最右侧元素
int j = i;
//当j > 0时循环
//判断j指向的元素与其左侧元素的大小
while (j > 0 && array[j] < array[j - 1]) {
//当左侧元素j-1更大时,执行换位
int t = array[j];
array[j] = array[j - 1];
array[j - 1] = t;
//j自减,表示向左移动
j--;
}
}
System.out.println(Arrays.toString(array));
}
}
05-希尔排序(Shell sort)
1.介绍
- 希尔排序(Shell sort)的名称源自于它的发明者Donald Shello
- 它通过相距一定间隔的元素来工作,各轮对比所用的距离随着算法的进行而减小,走到只比较相邻元素的最后一轮排序为止,所以,它也叫作缩减增量排序(Diminishing increment sort)。
- 从某一程度来看,它是插入排序的升级版。
2.原理
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止 。
3.实现
package com.tt.test.demo1;
import java.util.Arrays;
public class ShellSort {
public static void main(String[] args) {
int[] arr = {8,1,4,9,0,3,5,2,7,6};
// ShellSort_swap(arr);
ShellSort_move(arr);
}
//交换式
public static void ShellSort_swap(int[] arr) {
int temp = 0;
//多轮循环:缩减增量
//初始条件:增量(gap)值为数组长度除以2
//循环条件:增量>0
//条件自变:增量自除2
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
//从与增量值大小相等的下标位置开始,向右循环
for (int i = gap; i < arr.length; i++) {
//步长为5(每组有两个元素)
//接下来的循环表示向左找"同组"的元素尝试对比及必要的换位
//j:左侧"同组"元素的下标,即被对比元素的下标
for (int j = i - gap; j >= 0; j -= gap) {
//如果当前元素大于加上步长后的那个元素,则交换
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
}
System.out.println("Shell排序后:" + Arrays.toString(arr));
}
//移位式的希尔排序
public static void ShellSort_move(int[] arr){
//增量的gap
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
int temp = 0;
int j = 0;
//从第gap个元素开始,逐个对其所在的组进行直接插入
for(int i = gap ; i < arr.length ; i++){
j = i;
temp = arr[j];
if(arr[j] < arr[j - gap]){
while (j - gap >= 0 && temp < arr[j - gap]){
//移动
arr[j] = arr[j - gap];
j -= gap;
}
//当退出while循环后,就给temp找到了插入位置
arr[j] = temp;
}
}
}
System.out.printf("Shell排序后:%s\n", Arrays.toString(arr));
}
}
06-归并排序(Merge sort)
1.介绍
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)
分治策略是一种非常经典的思想,它将问题分(divide)成一些小问题,然后递归求解,而治(conquer)的阶段会将分的阶段得到的问题解合并在一起。
递归:表现为在方法的内部,调用当前方法自身
2.原理
归并排序的核心思想是将2个未排序的“小”数组的所有元素有序的填充到一个新的数组中去,排序的同时形成“合并”。
3.实现
package com.tt.test.demo1;
import java.util.Arrays;
public class mergeSort {
public static void main(String[] args) {
int[] array = {8, 1, 4, 9, 0, 3, 5, 2, 7, 6};
int[] temp = new int[array.length]; // 归并排序需要一个额外空间
merges(array, 0, array.length - 1, temp);
System.out.println(Arrays.toString(array));
}
//分+合的方法
public static void merges(int[] array, int left, int right, int temp[]) {
if (left < right) {
int mid = (left + right) / 2;
// 中间索引
// 向左递归进行分解
merges(array, left, mid, temp);
// 向右递归进行分解
merges(array, mid + 1, right, temp);
// 合并
merge(array, left, mid, right, temp);
}
}
// 合并方法
// array,排序数组;left,左边有序序列的初始索引;right,右边有序序列的初始索引;mid,中间索引;temp,做中转的数组
public static void merge(int array[], int left, int mid, int right, int temp[]) {
int i = left;
int j = mid + 1;
int t = 0;// t表示temp中转数组的初始索引
// 第一步:先把左右两边有序的数据按照规则填充到temp数组,直到有一边处理完毕为止
while (i <= mid && j <= right) {
if (array[i] < array[j]) {
temp[t] = array[i];
i += 1;
t += 1;
} else {
temp[t] = array[j];
j += 1;
t += 1;
}
}
// 第二步:把剩余数据的一边的数据依次全部填充到temp
while (i <= mid) {
temp[t] = array[i];
i += 1;
t += 1;
}
while (j <= right) {
temp[t] = array[j];
j += 1;
t += 1;
}
// 第三步:将temp数组的元素拷贝到array
// 将 temp 数组的元素拷贝到 arr
// 注意,并不是每次都拷贝所有 t = 0; int tempLeft = left; //
// 第一次合并 tempLeft = 0 , right = 1 // tempLeft = 2 right = 3 // tL=0 ri=3 //最后一次
// tempLeft = 0 right = 7
t = 0;
int tempLeft = left;
while (tempLeft <= right) {
array[tempLeft] = temp[t];
t += 1;
tempLeft += 1;
}
}
}
07-快速排序(Quick sort)
1.原理
-
先挑选数组中的某个元素,它将作为所有元素排列大小的分界值
-
作为分界值的数组元素称之为:枢纽元 (pivot),也可称之为:主元
-
需要将比枢纽元小的元素放在其左侧位置,将比枢纽元大的元素放在其右侧位置。并不关心其左侧区域或右侧区域内的各元素是否有序
-
将原数组根据枢纽元划分开来的过程称之为:分区(Partition)
先从一堆数据中挑选出一个基准数,然后将比这个基准数小的数据全部放在基准数的前面,将比他大的放在基准数后面,此时,不管前后,都是无序的,然后再分别在前后两组数据中挑出一个基准数,重复此操作,一直分下去,分成两部分、四部分、八部分。。。直到每一个数据的左边都比他小,右边都比他大,此时整个数组就是有序的了。
常用的选取枢纽元的方案。
错误的做法
选取两端的某个元素作为枢纽元,在有序甚至降序的数组中,表现非常糟糕
有序或局部有序的原始数组并不罕见
安全的做法
随机选取枢纽元
因为几乎不可能每次都产生劣质的分区
但是,生成随机数也是有开销的,可能导致排序效率下降
三数中值分割法(Median-of-Three Partitioning)
使用最左侧、最右侧、中心位置的三个元素的中值作为枢纽
可以消除有序数组的坏情况
2.实现
public static void QuickSort(int[] arr, int start,int end ){
if(start<end){
int index=getIndex(arr,start,end);//定义getIndex方法传入参数
QuickSort(arr,start,index-1);//使用递归的方法,对基准数左边部分进行递归
QuickSort(arr, index+1, end);//对右边部分递归
}
}
private static int getIndex(int[] arr, int start, int end) {
int i=start;
int j=end;
//定义一个基准数
int x=arr[i];
while (i<j){
//从后往前找比他小的数的下标。
while (i < j&&arr[j]>x) {
j--; //如果比他大则向前移动,直到找到小的为止。
}
//将找到的数填到上一个数的位置。
if (i < j) {
arr[i] = arr[j];
i++;
}
//从前往后找比他大于等于的数的下标。
while (i < j && arr[i]<= x) {
i++;
}
if (i < j) {
arr[j] = arr[i];
j--;
}
}
arr[i]=x;
return i;
}
08_排序算法的选取
- 在一般情况下,可能比较关注算法所表现出来的运算效率,评估值主要包括:
- 判断次数
- 交换次数
- 耗时
- 可以随机生成50000个随机数构成的数组,并观察以上各指标。
注意:
-
一般不建议使用元素特别多的数组,因为有些算法的性能非常差;
一例如,使用冒泡排序来处理长度为50万的数组,整个排序过程可能需要7分钟以上(仅供参考)
-
由于随机数值大小、初始数组内元素的大小顺序、机器的性能等多方面原因,测试结果仅供参考;
一例如,在快速排序时,数组元素的顺序直接影响排序效率,另外,插入排序、希尔排序、归并排序也是这样
-
归并排序没有发生实质的交换,而是将原数组的元素有序的填充到新数组,只能使用“填充次数”替代“交换次数”作为参考。
-
另外,还必须关注算法的稳定性!