部分常见的排序算法:
- 1、冒泡排序
- 1.1、排序图解
- 1.2、代码体现
- 2、选择排序
- 2.1、排序图解
- 2.2、代码体现
- 3、插入排序
- 3.1、排序图解
- 3.2、代码体现
- 4、希尔排序
- 4.1、排序图解
- 4.2、代码体现
- 5、快速排序
- 5.1、排序图解
- 5.2、代码体现
- 6、归并排序
- 6.1、排序图解
- 6.2、代码体现
- 7、基数排序
- 7.1、排序图解
- 7.2、代码体现
- 7.3、关于基数排序的一些说明
- 8、以上排序算法的总结和对比
1、冒泡排序
冒泡排序的基本思想是:依次比较相邻元素的值,若发现逆序则交换,使较大的元素逐渐向后移动,就像是水底的气泡一样逐渐向上浮动。
1.1、排序图解
1.2、代码体现
public class BubbleSort {
public static void main(String[] args) {
int value[] = {3, -6, 18, 5, -11};
sort(value);
System.out.println("排序后 : " + Arrays.toString(value));
}
private static void sort(int[] value) {
int temp;//定义一个临时变量,在交换位置的时候使用
boolean flag = false;//定义一个标识,用于判断是否进行过交换,从而优化当前排序算法
for (int i = 1; i < value.length; i++) { //外层for循环控制循环的次数
for (int j = 0; j < value.length - i; j++) { //内存for循环控制比较的次数
if (value[j] > value[j + 1]) { //如果前一位数大于后一位,则交换它们的位置
flag = true;//说明此时有发生位置交换,将标识更改为true
temp = value[j + 1];//将较小值赋值给临时变量
value[j + 1] = value[j];//将较大值后移
value[j] = temp;//将较小值前移
}
}
//这里将其进行一个简单的优化:说明在某一次排序中,未进行任何的位置交换,也就表明当前的顺序已经是有序的了,从而减少不必要的比较
if (!flag) {
break;
} else {
flag = false;//将标识还原,便于后续继续使用
}
}
}
}
2、选择排序
选择排序的基本思想是:第一次从arr[0 ]— arr[n-1]中选取最小值,与arr[0]交换;第二次从arr[1] — arr[n-1]中选取最小值,与arr[1]交换;第三次从arr[2] — arr[n-1]中选取最小值中选取最小值,与arr[2]交换…以此类推,工通过n-1次后,得到一个有序序列。
2.1、排序图解
2.2、代码体现
//选择排序
public class SelectSort {
public static void main(String[] args) {
int[] value = {3, -6, 18, 5, -11};
sort(value);
System.out.println("排序后 : " + Arrays.toString(value));
}
private static void sort(int[] value) {
int minIndex;//假定的最小下标
int minValue;//假定的最小值
for (int i = 0; i < value.length - 1; i++) { //外层for循环控制循环的次数
minIndex = i;
minValue = value[i];
for (int j = i + 1; j < value.length; j++) { //内层for循环控制比较的次数
if (minValue > value[j]) { //说明假定的最小值并不是最小的
minValue = value[j];//将这个最小值重置
minIndex = j;//重置最小下标
}
}
//如果当前下标和假定的最小下标不相等,则说明需要交换这两个值
if (minIndex != i) {
value[minIndex] = value[i];
value[i] = minValue;
}
}
}
}
3、插入排序
插入排序的基本思想是:把n个待排元素看成一个有序表和一个无序表,开始的时候有序表中只有一个元素,无序表中有n-1个元素,排序过程中每次从无序表中取出第一个元素,将它的排序码依次和有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
3.1、排序图解
3.2、代码体现
//插入排序
public class InsertSort {
public static void main(String[] args) {
int[] value = {3, -6, 18, 5, -11};
sort(value);
System.out.println("排序后 : " + Arrays.toString(value));
}
private static void sort(int[] value) {
int insertVal;//定义待插入的数
int insertIndex;//定义待插入的下标,即当前元素的前一个元素的下标
for (int i = 1; i < value.length; i++) { //下标i从1开始,因为是从第二个数开始插入的
insertVal = value[i];
insertIndex = i - 1;
//insertIndex >= 0 其作用是保证在给insertVal找插入位置的时候不会发生下标越界
//insertVal < value[insertIndex] 其作用是判断插入的数是不是小于前一个数
while (insertIndex >= 0 && insertVal < value[insertIndex]) {
//此时说明要插入的数的前一个数大于它,所以需要将这个大数后移
value[insertIndex + 1] = value[insertIndex];
insertIndex--;//下标后移,进行后续操作
}
//当退出循环时,表名要插入的位置已经找到,此时需要将insertIndex + 1后移
//因为待插入元素本身就有可能在当前位置,所以先判断一下是否需要赋值
if (insertIndex + 1 != i) {
value[insertIndex + 1] = insertVal;//将待插入的数赋值到该位置
}
}
}
}
4、希尔排序
希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
其基本思想是:把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止
4.1、排序图解
4.2、代码体现
在进行排序时,有两种方法可供选择:交换法和移动法,如下所示
//希尔排序
public class ShellSort {
public static void main(String[] args) {
int[] value = {-3, 6, 8, -5, 0, -11};
//sort_1(value);
//System.out.println("方法一排序后 : " + Arrays.toString(value));
sort_2(value);
System.out.println("方法二排序后 : " + Arrays.toString(value));
}
//方法一:在插入时采用交换法(该方法过于耗时,推荐使用第二种方式:移动法)
private static void sort_1(int[] value) {
int temp;//定义一个临时变量,作交换时使用
for (int gap = value.length / 2; gap > 0; gap /= 2) { //gap:步长。外层for控制循环次数
for (int i = gap; i < value.length; i++) { //当前for控制每次比较的次数
for (int j = i - gap; j >= 0; j -= gap) {
//如果当前元素大于加上步长后的那个元素,则进行交换
if (value[j] > value[j + gap]) {
temp = value[j];
value[j] = value[j + gap];
value[j + gap] = temp;
}
}
}
}
}
//方法二:在插入时采用移动法,其本质就是结合了插入排序的思想
private static void sort_2(int[] value) {
for (int gap = value.length / 2; gap > 0; gap /= 2) {
//直接从gap个元素开始,逐个对其所在的组进行直接插入排序
for (int i = gap; i < value.length; i++) {
int j = i;//将当前下标先临时记录一下
int temp = value[j];//将当前值也一并记录下来
//j - gap其作用是为了防止下标越界
while (j - gap >= 0 && temp < value[j - gap]) {
//temp < value[j - gap]:如果当前值小于所在组的另一个元素时,将其位置对换
value[j] = value[j - gap];
j -= gap;//拿到要插入的位置下标
}
//当退出while循环时,此时temp就找到了要插入的位置
value[j] = temp;
}
}
}
}
5、快速排序
快速排序是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
5.1、排序图解
5.2、代码体现
//快速排序
public class QuickSort {
public static void main(String[] args) {
int[] value = {3, -6, 18, 5, -11};
sort(value, 0, value.length - 1);
System.out.println("排序后 : " + Arrays.toString(value));
}
/**
* 将给定的数组进行有序的排序
* @param value 要进行排序的数组
* @param left 左索引
* @param right 右索引
*/
private static void sort(int[] value, int left, int right) {
int l = left;
int r = right;
//pivot是用来做分割的数
int pivot = value[(l + r) / 2];
int temp;//定义一个临时变量,在做交换的时候使用
//while循环的目的是为了让比pivot值小的放到左边,比pivot值大的放到右边
while (l < r) {
//在pivot的左边一直找,找到大于等于pivot的值才退出
while (value[l] < pivot) {
l++;
}
//在pivot的右边一直找,找到小于等于pivot的值才退出
while (value[r] > pivot) {
r--;
}
//如果l>=r说明pivot的左右两边的值都已经按照:左边全部都是小于等于pivot的值右边全部都是大于等于pivot的值放好了
if (l >= r) {
break;
}
//交换
temp = value[l];
value[l] = value[r];
value[r] = temp;
//如果交换完之后,发现arr[l] == pivot,则r--,向前移一步,避免造成死循环
if (value[l] == pivot) {
r--;
}
//如果交换完之后,发现arr[r] == pivot,则l++,向后走一步,同样是为了避免造成死循环
if (value[r] == pivot) {
l++;
}
}
//如果l==r,必须l++,r--,否则会出现栈溢出
if (l == r) {
l++;
r--;
}
//向左递归,将pivot左边的数进行有序处理
if (left < r) {
sort(value, left, r);
}
//向右递归,将pivot右边的数进行有序处理
if (right > l) {
sort(value, l, right);
}
}
}
6、归并排序
归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
6.1、排序图解
6.2、代码体现
//归并排序
public class MergeSort {
public static void main(String[] args) {
int[] value = {3, -6, 18, 5, -11};
int[] temp = new int[value.length];//归并排序需要一个额外空间
sort(value, 0, value.length - 1, temp);
System.out.println("排序后 : " + Arrays.toString(value));
}
private static void sort(int[] value, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2;//拿到中间索引
//向左递归进行分解
sort(value, left, mid, temp);//left即为左边序列起始下标,mind即为最终下标
//向右递归进行分解
sort(value, mid + 1, right, temp);//mind+1即为右边序列起始下标,right即为最终下标
//合并
merge(value, left, mid, right, temp);
}
}
/**
* 用于对上一步分解的结果进行合并
*
* @param value 原始数据
* @param left 左边有序序列的索引
* @param mid 中间索引
* @param right 右边有序序列的索引
* @param temp 做中转的数组
*/
public static void merge(int[] value, int left, int mid, int right, int[] temp) {
int l = left;//初始化l,即左边有序序列的初始索引
int r = mid + 1;//初始化r,即右边有序序列的初始索引
int index = 0;//指向temp数组的当前索引
//1、先把左右两边的有序数据按照规则填充到temp数组中,直到左右两边的有序序列,有一边处理完毕为止
while (l <= mid && r <= right) {
//如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素,就将左边的当前元素,拷到temp数组中
if (value[l] <= value[r]) {
temp[index] = value[l];//将左边的当前元素,拷到temp数组中
index++;//将temp的当前索引前移
l++;//左边序列索引前移
} else { //反之,将右边有序序列的当前元素拷到temp中
temp[index] = value[r];//将右边的当前元素,拷到temp数组中
index++;//将temp的当前索引前移
r++;//右边序列索引前移
}
}
//2、把剩余数据的一边的数据按顺序依次填充到temp中
while (l <= mid) {
//说明左边的有序序列中还有剩余的元素,将其填充到temp中
temp[index] = value[l];
index++;
l++;
}
while (r <= right) {
//说明右边的有序序列中还有剩余的元素,将其填充到temp中
temp[index] = value[r];
index++;
r++;
}
//3、将temp数组的元素拷贝到原始数组value中。此时需要注意的是:并不是每次都会将所有的数据都会拷贝过去
index = 0;
int tempLeft = left;
while (tempLeft <= right) {
value[tempLeft] = temp[index];
index++;
tempLeft++;
}
}
}
7、基数排序
基数排序属于“分配式排序”,又称“桶子法”,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用 ;
基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法 ;
基数排序是桶排序的扩展
基数排序的基本思想:将整数按位数切割成不同的数字,然后按每个位数分别比较。将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
7.1、排序图解
7.2、代码体现
//基数排序
public class RadixSort {
public static void main(String[] args) {
int[] value = {15, 8, 121, 99, 0, 9,1};
sort(value);
System.out.println("排序后 : " + Arrays.toString(value));
}
private static void sort(int[] value) {
//首先得到数组中最大的数的位数
int max = value[0];//假设第一个数就是最大的数
for (int i = 1; i < value.length; i++) {
if (value[i] > max) {
max = value[i];//找到实际的最大数
}
}
//判断当前最大数是几位数,它决定了我们的排序算法最终循环的次数
int maxLength = (max + "").length();
/*这里定义一个二维数组,表示10个桶,每个桶就是一个一位数组。为了防止在放入数据的时候,出现溢出现象
则将每一个一位数组大小定为value.length,很明显,基数排序用到的就是空间换时间这一思路*/
int[][] bucket = new int[10][value.length];
/*为了记录每个桶中,实际存放了多少个数据,还需要定义一个一位数组,来记录各个桶每次放入的数据个数*/
int[] bucketElementCounts = new int[10];//eg:bucketElementCounts[0],记录的就是第一个桶中放入的数据个数
//开始对数据进行排序处理,这里n的作用是为了取出每个数的各个位上的数字时需要用到的
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
//对每个元素的对应位进行排序处理,第一次是个位,第二次是十位,以此类推.....
for (int j = 0; j < value.length; j++) {
int digitOfElement = value[j] / n % 10;//取出的每个元素对应位的值
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = value[j];//将该值放入对应的桶中
bucketElementCounts[digitOfElement]++;//记录当前桶中的数据个数
}
int index = 0;//在放回原数组时需要用到的下标
//遍历每一个桶,并将桶中的数据,放入到原数组中
for (int k = 0; k < bucketElementCounts.length; k++) {
//如果桶中有数据,我们才放入到原数组
if (bucketElementCounts[k] != 0) {
//循环这个桶(即第k个一维数组)
for (int l = 0; l < bucketElementCounts[k]; l++) {
//取出当前桶中的数据,放入原数组去
value[index] = bucket[k][l];
index++;//下标后移
}
}
//每一轮处理结束后,需要将当前bucketElementCounts[k]置零,便于后续继续计数使用
bucketElementCounts[k] = 0;
}
}
}
}
7.3、关于基数排序的一些说明
1、基数排序是对传统桶排序的扩展,速度很快;
2、 基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 OutOfMemoryError ;
3、 基数排序是稳定的。[注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的] ;
4、当前基数排序不支持含有负数的数组,如果需要支持负数操作,请参考其它博文。
8、以上排序算法的总结和对比
相关术语解释:
1、稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
2、不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
3、内排序:所有排序操作都在内存中完成;
4、外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
5、时间复杂度: 一个算法执行所耗费的时间。
6、空间复杂度:运行完一个程序所需内存的大小。
7、 n: 数据规模
8、 k: “桶”的个数