冒泡排序:每次使用当前的数据a与当前数据的后一个数据b比较,如果a > b, 则交换;反之,不交换,第一次循环下来,最右边的值就是本数组内的最大值,再进行之后的循环,比较其他的数据,如此将大的数以此排放至右侧,实现从左到右的从小到大数据排列;
/**
* 冒泡排序:每次使用当前的数据a与当前数据的后一个数据b比较,如果a > b, 则交换;反之,不交换,
* 第一次循环下来,最右边的值就是本数组内的最大值,在进行之后的循环,比较其他的数据,
* 如此将大的数以此排放至右侧,实现从左到右的从小到大数据排列
*
* 如,原始数据为:{1, 5, 8, 0, 99, 52, 31}
* 第一次循环:
* 第一次比较:"1", "5", 8, 0, 99, 52, 31 (1 < 5, 不交换)
* 第二次比较:1, "5", "8", 0, 99, 52, 31 (5 < 8, 不交换)
* 第三次比较:1, 5, "8", "0", 99, 52, 31 (8 > 0, 交换)
* 第四次比较:1, 5, 0, "8", "99", 52, 31 (8 < 99, 不交换)
* 第五次比较:1, 5, 0, 8, "99", "52", 31 (99 > 52, 交换)
* 第六次比较:1, 5, 0, 8, 52, "99", "31" (99 > 31, 交换)
* 第一次循环比较的结果:{1, 5, 0, 8, 52, 31, 99}
*
* 此时待比较的数据:{1, 5, 0, 8, 52, 31}
* 第二次循环:
* 第一次比较:"1", "5", 0, 8, 52, 31 (1 < 5, 不交换)
* 第二次比较:1, "5", "0", 8, 52, 31 (5 > 0, 交换)
* 第三次比较:1, 0, "5", "8", 52, 31 (5 < 8, 不交换)
* 第四次比较:1, 0, 5, "8", "52", 31 (8 < 52, 不交换)
* 第五次比较:1, 0, 5, 8, "52", "31" (52 > 31, 交换)
* 第二次循环比较的结果:{1, 0, 5, 8, 31, 52}
*
* 此时待比较的数据:{1, 0, 5, 8, 31}
* 第三次循环:
* 第一次比较:"1", "0", 5, 8, 31 (1 > 0, 交换)
* 第二次比较:0, "1", "5", 8, 31 (1 < 5, 不交换)
* 第三次比较:0, 1, "5", "8", 31 (5 < 8, 不交换)
* 第四次比较:0, 1, 5, "8", "31" (8 < 31, 不交换)
* 第三次循环比较的结果:{0, 1, 5, 8, 31}
*
* 此时待比较的数据:{0, 1, 5, 8}
* 第四次循环:
* 第一次比较:"0", "1", 5, 8 (0 < 1, 不交换)
* 第二次比较:"0", "1", 5, 8 (0 < 1, 不交换)
* 第三次比较:"0", "1", 5, 8 (0 < 1, 不交换)
* 第四次循环比较的结果:{0, 1, 5, 8}
*
* 此时待比较的数据:{0, 1, 5}
* 第五次循环:
* 第一次比较:"0", "1", 5 (0 < 1, 不交换)
* 第二次比较:0, "1", "5" (1 < 5, 不交换)
* 第五次循环比较多结果:{0, 1, 5}
*
* 此时待比较的数据:{0, 1}
* 第六次循环:
* 第一次比较:"0", "1" (0 < 1, 不交换)
* 第六次循环比较的结果:{0, 1}
*
* 综上分析:冒泡排序算法共进行 (数组的长度 - 1) 趟排序,每趟排序共进行 (数组长度 - 当前趟排序的次数) 次比较
*
* 最终排序结果:{0, 1, 5, 8, 31, 52, 99}
* @param source 待排序的数组
*/
public static void bubbleSort(int[] source) {
// TODO 第i趟排序
for(int i = 0; i < source.length - 1; i++) {
// TODO 本次排序比较 数组长度-i次
for(int j = 0; j < source.length - 1 - i; j++) {
if(source[j] > source[j + 1]) { // 如果当前元素 大于 当前元素的后一个元素
int tmp = source[j];
source[j] = source[j + 1];
source[j + 1] = tmp;
}
}
// System.out.println("第" + (i+1) + "趟排序");
}
// System.out.println("共进行" + (source.length - 1) + "趟排序");
}
我们分析一下,如果某次循环时,一次都没有进行交换,那么这一个区域的数据一定是有序的!,那么我们后面的遍历比较都是无意义的(所以就直接结束排序方法),我们可以在这里着手优化,以增强排序的效率,如下代码:
/**
* 该方法是优化后的冒泡排序
* @param source
*/
public static void bubbleSortOptimization(int[] source) {
for(int i = 0; i < source.length - 1; i++) {
boolean exchanged = false; // 用来保存当前趟排序中是否进行过元素交换(即当前趟排序中出现过顺序不对的元素)
for(int j = 0; j < source.length - 1 - i; j++) {
if(source[j] > source[j + 1]) {
int tmp = source[j];
source[j] = source[j + 1];
source[j + 1] = tmp;
exchanged = true;
}
}
// System.out.println("第" + (i+1) + "趟排序");
if(!exchanged) { // 如果本趟排序没有交换过元素(即元素此时为有序的,就不用进行接下来的排序了)
// System.out.println("共进行" + (i + 1) + "趟排序");
break;
}
}
}
每次从待排序的数据中找到值最小的数据,将此数据与待排序数据(未进行交换的区域)的最左边数据进行交换,直至左边的数据都经过比较,相比于冒泡排序,选择排序的效率更高,高在交换次数上,因为选择排序每次的交换都是有意义的;
/**
* 选择排序:每次从待排序的数据中找到值最小的数据,将此数据与待排序数据的最左边数据进行交换,直至左边的数据都经过比较
* 相比于冒泡排序,选择排序的效率更高,高在交换次数上,因为选择排序每次的交换都是有意义的
*
* 如:原始数据为:{0, 5, 88, 3, 6, 7}
* 第一次循环:"0", 5, 88, 3, 6, 7 (最左边的数据为0,其下标为0;最小数据为0,其下标为0,进行交换)
* 第一次循环结果:{0, 5, 88, 3, 6, 7}
*
* 此时待排序的数据为:{5, 88, 3, 6, 7}
* 第二次循环:5, 88, "3", 6, 7 (最左边数据为5,其下标为1;最小数据为3,其下标为3,进行交换)
* 第二次循环结果:{3, 88, 5, 6, 7}
*
* 此时待排序数据为:{88, 5, 6, 7}
* 第三次循环:88, "5", 6, 7 (最左边数据为88,其小标为2;最小数据为5,其小标为3,进行交换)
* 第三次循环结果:{5, 88, 6, 7}
*
* 此时待排序数据为:{88, 6, 7}
* 第四次循环:88, "6", 7 (最左边数据为88,其下标为3;最小数据为6,其下标为4,进行交换)
* 第四次循环结果:{6, 88, 7}
*
* 此时待排序数据为:{88, 7}
* 第五次循环:88, "7" (最左边数据为88,其下标为4;最小数据为7,其下标为5,进行交换)
* 第五次循环结果:{7, 88}
*
* 最终结果,从左至右从小到大为:{0, 3, 5, 6, 7, 88}
*
* 分析:待排序区数据的最左边数据的下标可以通过for循环取到,
*
* @param source 待排序的数组
*/
public static void selectSort(int[] source) {
// TODO 1、进行 数组长度 - 1次 排序
for(int i = 0; i < source.length - 1; i++) {
// TODO 2、猜想待排序区的第一个元素为最小值,并将其下标定义为最小值的下标
int minValue = source[i];
int minValueIndex = i;
// TODO 3、从待排序区的第二个元素开始遍历
for(int j = i + 1; j < source.length; j++) {
if(minValue > source[j]) { // 如果猜想的最小值 大于 待排序中的任何一个元素
// TODO 即猜想错误,将最小值和最小值下标修改
minValue = source[j]; // 修改此变量为最小值
minValueIndex = j; // 修改此变量为最小值下标
}
/* 用来降序排列
if(minValue < source[j]) { // 如果猜想的最大值 小于 待排序中的任何一个元素
// TODO 即猜想错误,将最大值和最大值下标修改
minValue = source[j]; // 修改此变量为最大值
minValueIndex = j; // 修改此变量为最大值下标
}*/
}
// TODO 4、如果最小值下标 不等于 一开始猜想的待排序区的第一个元素
if(minValueIndex != i) {
// 将修改后的猜想值 与 待排序区的值进行交换
source[minValueIndex] = source[i]; // 将 待排序区内的最小值位置的值 修改为 之前判断的待排序区的第一个元素的值
source[i] = minValue; // 将 待排序区的第一个元素的值修改为 待排序区内的最小值
}
}
}
须知:插入排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的;
把n个待排序的元素看成为一个有序区域和一个无序区域,开始时有序区域只包含第一个元素,无序区域中包含后续的n-1个元素,排序过程中每次从无序区域取出第一个元素,然后把它的排序码依次与有序区域的元素的排序码进行比较,以将它插入到有序区域中的适当位置,使之称为新的有序区域,依次循环,直到无序区域内没有元素,此时就排序完成了;从下标为1位置(第二个元素)开始
,共进行数组长度-1次插入
;
/**
* 插入排序:插入排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的;
*
* 基本思想:把n个待排序的元素看成为一个有序区域和一个无序区域,开始时有序区域只包含第一个元素,无序区域中包含
* 后续的n-1个元素,排序过程中每次从无序区域取出第一个元素,然后把它的排序码依次与有序区域的元素的排序码进行比
* 较,以将它插入到有序区域中的适当位置,使之称为新的有序区域,依次循环,直到无序区域内没有元素,此时就排序完
* 成了;从下标为1位置(第二个元素)开始,共进行数组长度-1次插入;
*
* 如:原始数据为:{0, 88, 5, 3, 6, 7}
*
*
* 第一次:"0", 88, 5, 3, 6, 7 (有序区域:0;无序区域:88, 5, 3, 6, 7;无序区域的第一个值:88;插入到有序区域的索引位置:1)
* 第一次结果:"0", "88", 5, 3, 6, 7
*
* 第二次:"0", "88", 5, 3, 6, 7 (有序区域:0, 88;无序区域:5, 3, 6, 7;无序区域的第一个值:5;插入到有序区域的索引位置:1)
* 第二次结果:"0", "5", "88", 3, 6, 7
*
* 第三次:"0", "5", "88", 3, 6, 7 (有序区域:0, 5, 88;无序区域:3, 6, 7;无序区域的第一个值:3;插入到有序区域的索引位置:1)
* 第三次结果:"0", "3", "5", "88", 6, 7
*
* 第四次:"0", "3", "5", "88", 6, 7 (有序区域:0, 3, 5, 88;无序区域:6, 7;无序区域的第一个值:6;插入到有序区域的索引位置:3)
* 第四次结果:"0", "3", "5", "6", "88", 7
*
* 第五次:"0", "3", "5", "6", "88", 7 (有序区域:0, 3, 5, 6, 88);无序区域:7;无序区域的第一个值:7;插入到有序区域的索引位置:4)
* 第五次结果:"0", "3", "5", "6", "7", "88"
*
* @param source 待排序的数组
*/
public static void insertSort(int[] source) {
// TODO 遍历无序区域,默认从1开始(即从第二个元素开始,即依次获取无序区域的第一个元素)
for(int i = 1; i < source.length; i++) {
int insertPreValueIndex = i - 1; // 获取要插入有序区域位置的前一个位置的索引(默认为有序区域的最后一个元素的索引)
int insertValue = source[i]; // 获取要插入的元素(默认为无序区域的第一个元素)
// TODO 通过此循环找到要插入到无序区域的索引位置
while(insertPreValueIndex >= 0 // 如果 要插入有序区域位置的前一个位置的索引 大于等于0,即保证数组不越界
// && source[insertPreValueIndex] > insertValue // 并且 如果要插入有序区域位置的前一个位置的值 大于 要插入元素的值 (升序使用)
&& source[insertPreValueIndex] < insertValue // 并且 如果要插入有序区域位置的前一个位置的值 小于 要插入元素的值 (降序使用)
) {
source[insertPreValueIndex + 1] = source[insertPreValueIndex]; // 将要插入有序区域位置的前一个位置的值赋值到其后一个元素
insertPreValueIndex--; // 将要插入有序区域位置的前一个位置的索引--
// System.out.println("--; insertPreValueIndex = " + insertPreValueIndex + "; insertValue = " + insertValue);
}
// TODO 如果 要插入有序区域位置的前一个位置的索引+1 不等于 无序区域的第一个元素的索引,此时我们需要进行手动插入;
if(insertPreValueIndex + 1 != i) {
// TODO 程序执行到此,说明找到了 要插入有序区域的位置(insertPreValueIndex + 1)
// System.out.println("插入位置:" + (insertPreValueIndex + 1) + "; insertValue = " + insertValue + "\n");
source[insertPreValueIndex + 1] = insertValue; // 将 要插入的元素 插入到 要插入有序区域的位置
}
}
}
须知:希尔排序是一种插入排序
,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序;
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减少至1时,整个文件恰好被分成一组,此时就排序完成了;
/** 希尔排序(对有序序列插入时采用交换法)
* 希尔排序:是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序;
*
* 基本思想:希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量
* 减少至1时,整个文件恰好被分成一组,此时就排序完成了;
*
* 如:原始数据:{5, 6, 2, 9, 7, 1, 8, 4, 11, 3}
*
* 初始增量:数组长度(10)/2 = 5,即整个数组被分成5组,步长为5;
*
* 第一趟:增量:5,分为5组,步长为5
* 即:5, 6, 2, 9, 7, 1, 8, 4, 11, 3 分组为:(5, 1)、(6, 8)、(2, 4)、(9, 11)、(7, 3)
* 对每一组进行直接插入排序:
* 每组变成:(1, 5)、(6, 8)、(2, 4)、(9, 11)、(3, 7)
* 因此第一次的结果:1, 6, 2, 9, 3, 5, 8, 4, 11, 7
*
* 第二趟:增量:上次的增量(5)/2 = 2,分为2组,步长为2
* 即:1, 6, 2, 9, 3, 5, 8, 4, 11, 7 分组为:(1, 2, 3, 8, 11)、(6, 9, 5, 4, 7)
* 对每一组进行直接插入排序:
* 每组变成:(1, 2, 3, 8, 11)、(4, 5, 6, 7, 9)
* 因此第二次结果:1, 4, 2, 5, 3, 6, 8, 7, 11, 9
*
* 第三趟:增量:上次的增量(2)/2 = 1,分为1组,步长为1
* 即:1, 4, 2, 5, 3, 6, 8, 7, 11, 9 分组为:(1, 4, 2, 5, 3, 6, 8, 7, 11, 9)
* 对每一组进行直接插入排序:
* 每组变成:(1, 2, 3, 4, 5, 6, 7, 8, 9, 11)
*
* 此时排序完成
*
* @param source 待排序数组
*/
public static void shellSort(int[] source) {
// int count = 0;
// TODO 计算本趟的增量
for(int increment = source.length / 2; increment > 0; increment /= 2) {
// TODO 获取增量后的元素的下标,然后遍历值数组长度-1处(即数组的最后一个元素),此是遍历的次数就是本趟排序所分的组数
for(int i = increment; i < source.length; i++) {
// TODO 根据增量对各组的元素进行排序,步长为 increment
for(int j = i - increment; j >= 0; j -= increment) {
// System.out.println("i = " + i + "; j = " + j);
// TODO 如果当前元素值 大于 加上步长后的那个元素值
if(source[j] > source[j + increment]) {
// TODO 进行交换
int tmp = source[j];
source[j] = source[j + increment];
source[j + increment] = tmp;
// System.out.println("交换:j = " + j + "-->" + source[j] + "; increment + j = " + (j + increment) + "-->" + source[j + increment]);
}
}
}
// System.out.println("第" + (++count) + "趟排序结果:" + Arrays.toString(source) + "\n\n");
}
}
上述的方式实现希尔排序是通过if判断,然后交换元素
的方式(会极大的消耗我们的排序时间)实现每一组的有序性,但是如果有大量的待排序元素通过该方式来保证每组的有序性实际上效率还是略低的,并没有将我们理论上的高效率体现出来,因此我们做出如下优化,通过移动法
来实现,代码如下:
/** 对希尔排序的优化(对有序序列插入时采用移动法)
*
* @param source 待排序数组
*/
public static void shellSortOptimization(int[] source) {
for(int increment = source.length / 2; increment > 0; increment /= 2) {
for(int i = increment; i < source.length; i++) {
int insertValueIndex = i;
int insertValue = source[insertValueIndex];
// if(source[insertValueIndex] < source[insertValueIndex - increment]) { // 升序
// while(insertValueIndex - increment >= 0 // 防止数组下标越界
// && insertValue < source[insertValueIndex - increment]
// ) {
if(source[insertValueIndex] > source[insertValueIndex - increment]) { // 降序
while(insertValueIndex - increment >= 0 // 防止数组下标越界
&& insertValue > source[insertValueIndex - increment]
) {
// 移动
source[insertValueIndex] = source[insertValueIndex - increment];
insertValueIndex -= increment;
}
// 程序执行到此,说明此时找到了插入的位置
source[insertValueIndex] = insertValue;
}
}
}
}
须知:快速排序:是对冒泡排序的一种改进
通过一趟排序将要排序的数据分割为独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据再分别快速排序,整个排序过程可以使用递归,以此达到整个数据变成有序序列;
/** 快速排序:是对冒泡排序的一种改进。
*
* 基本思想:通过一趟排序将要排序的数据分割为独立的两部分,其中一部分的所有数据都比另外一部分的所
* 有数据都要小,然后再按此方法对这两部分数据再分别快速排序,整个排序过程可以使用递归,以此达到整
* 个数据变成有序序列;
*
* @param source 待排序数组
* @param left 本次快排的左边界下标
* @param right 本次快排的右边界下标
*/
public static void quickSort(int[] source, int left, int right) {
int lp = left; // 本次快排的左侧指针,默认为本次快排的左边界下标
int rp = right; // 本次快排的右侧指针,默认为本次快排的右边界下标
int referenceValue = source[(left + right) / 2]; // 基准值(以本次快排的中间位置的值为本次快排的基准值)
// TODO 让本次快排 比本次快排基准值小的值放在基准值左边;让本次快排 比本次快排基准值大的放在基准值右边
while(lp < rp) { // 如果本次快排的左指针 < 本次快排的右指针,就一直进行此循环
System.out.println("-----------------------------");
System.out.println("开始:lp = " + lp + "(v:" + source[lp] + "); rp = " + rp + "(v:" + source[rp] + "); bp = " + (left + right) / 2 + "(v:" + referenceValue + ");");
// TODO 在本次快排基准值的左边一直向右找,直到找到大于等于基准值的值为止
while(source[lp] < referenceValue) {
lp++; // 本次快排的左侧指针后移
}
// TODO 在本次快排基准值的右边一直向左找,直到找到小于等于基准值的值为止
while(source[rp] > referenceValue) {
rp--; // 本次快排的右侧指针前移
}
/* TODO 如果本次快排左侧指针的位置 >= 本次快排右侧指针的位置;
* 那么此时 本次快排基准值的左边一定都是小于等于基准值的,本次快排基准值的右边一定都是大于等于基准值的,
* 因此本次快排目的达成,结束循环
*/
if(lp >= rp) {
System.out.println();
System.out.println();
break;
}
/* TODO 程序执行到此,说明上面退出循环的条件不满足(不满足本次快排基准值左侧都比基准值小,右侧都比基准
* 值大);出现此情况的原因是 本次快排左侧找到了一个比基准值的数据,右侧找到了一个比基准值小的数据,那么
* 此时应该将这两个值进行交换,然后再进行此循环(本次快排基准值的左边一直向右找,直到找到大于等于基准值
* 的值为止)
*/
System.out.println("交换:lp = " + lp + "(v:" + source[lp] + "); rp = " + rp + "(v:" + source[rp] + "); bp = " + (left + right) / 2 + "(v:" + referenceValue + ");");
int tmp = source[lp];
source[lp] = source[rp];
source[rp] = tmp;
/* TODO 交换之后,如果发现source[lp] == 本次快排的基准值,则将rp--;(因为source[lp] == 本次快排的基
* 准值,会发现进入下一次循环时,左侧指针和右侧指针都是不满足上面的while(source[lp] < referenceValue)
* 和while(source[rp] > referenceValue)条件的,此时lp和rp不能移动,导致程序死循环);
* 但是左侧指针所指的值是等于本次快排的基准值的,所以左侧指针此时不能移动,那么就要将右侧指针向前移动,
* 保证最后能够通过 if(lp >= rp) 条件跳出循环;
*/
if(source[lp] == referenceValue) {
rp--;
}
/* TODO 交换之后,如果发现source[rp] == 本次快排的基准值,则将lp++;(因为source[rp] == 本次快排的基
* 准值,会发现进入下一次循环时,左侧指针和右侧指针都是不满足上面的while(source[lp] < referenceValue)
* 和while(source[rp] > referenceValue)条件的,此时lp和rp不能移动,导致程序死循环);
* 但是右侧指针所指的值是等于本次快排的基准值的,所以右侧指针此时不能移动,那么就要将左侧指针向后移动,
* 保证最后能够通过 if(lp >= rp) 条件跳出循环;
*/
if(source[rp] == referenceValue) {
lp++;
}
System.out.println("交换后:lp = " + lp + "(v:" + source[lp] + "); rp = " + rp + "(v:" + source[rp] + "); bp = " + (left + right) / 2 + "(v:" + referenceValue + ");");
}
/* TODO 程序执行到此,说明本次快排基准值的左边一定都是小于等于基准值的,本次快排基准值的右边一定都是大于等
* 于基准值的,因此本次快排目的达成,此时应该进行递归
*/
// 先判断 本次快排的左侧指针位置 是否等于 本次快排右侧指针的位置,如果等于,应该将左侧指针后移,右侧指针前移(即让这两个指针错开),否则会出现栈溢出
if(lp == rp) {
lp++; // 本次快排的左侧指针后移
rp--; // 本次快排的右侧指针前移
}
// TODO 向左递归
if(left < rp) { // 如果本次快排的右侧指针的位置 大于 本次快排的左边界下标
quickSort(source, left, rp); // 进行递归
}
// TODO 向右递归
if(right > lp) { // 如果本次快排的左侧指针的位置 小于 本次快排的右边界下标
quickSort(source, lp, right); // 进行递归
}
}
归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略(分治法将问题分成一些小的问题然后递归求解,而“治”的阶段则将“分”的阶段得到的各答案“修补”在一起,即分而治之)
/** 归并排序:归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略(分治法将问题分成一些
* 小的问题然后递归求解,而“治”的阶段则将“分”的阶段得到的各答案“修补”在一起,即分而治之)
*
* @param source 待排序数组
* @param left 待排序数组的开始索引(即0)
* @param right 待排序数组的结尾所有(即 数组长度 - 1)
* @param tmp 中转数组
*/
public static void mergeSort(int source[], int left, int right, int tmp[]) {
if (left < right) {
int mid = (left + right) / 2; // 中间索引
// TODO 向左递归进行分解
mergeSort(source, left, mid, tmp);
// TODO 向右递归进行分解
mergeSort(source, mid + 1, right, tmp);
// TODO 合并排序
merge(source, left, mid, right, tmp);
}
}
/**
* 合并的方法
* @param array 排序的原始数组
* @param left 左边有序序列的初始索引(即原始数组array的开始索引)
* @param mid 中间索引(即原始数组array的 (开始索引 + 结束索引) / 2)
* @param right 右边有序序列的结束索引(即原始数组array的结束索引)
* @param tmp 做中转的数组
*/
private static void merge(int array[], int left, int mid, int right, int[] tmp) {
// System.out.println("进行合并");
int lp = left; // 左边有序序列的指针索引(默认为左边有序序列的初始索引)
int rp = mid + 1; // 右边有序序列的指针索引(默认为中间索引 + 1,即右边有序序列的初始索引)
int tp = 0; // 指向中转数组的索引,用来通过该变量将元素保存到中转数组
// TODO 一、先把左边有序序列 和 右边有序序列 按照规则 拷贝到中转数组,直到该两个有序序列有一个处理完毕为止
while (lp <= mid && rp <= right) { // 即两个有序序列有一个处理完毕为止
if (array[lp] <= array[rp]) { // 如果左边有序序列的元素 大于等于 右边有序序列的元素
tmp[tp++] = array[lp++]; // 将此时左边有序序列的元素 拷贝到 中转数组,然后将中转数组的指针后移,左边有序序列的指针后移
}
else { // 否则,即左边有序序列的元素 小于 右边有序序列的元素
tmp[tp++] = array[rp++]; // 将此时右边有序序列的元素 拷贝到 中转数组,然后将中转数组的指针后移,右边有序序列的指针后移
}
}
// TODO 二、将未处理完的一边的有序序列的元素 依次全部拷贝到中转数组
while(lp <= mid) { // 如果左边的有序序列 有剩余的未处理的元素
tmp[tp++] = array[lp++]; // 将此时左边有序序列的元素 拷贝到 中转数组,然后将中转数组的指针后移,左边有序序列的指针后移
}
while(rp <= right) { // 如果右边的有序序列 有剩余的未处理的元素
tmp[tp++] = array[rp++]; // 将此时右边有序序列的元素 拷贝到 中转数组,然后将中转数组的指针后移,右边有序序列的指针后移
}
/* TODO 三、将中转数组拷贝到 原始数组array
* 但是需要注意:并非是每次都拷贝所有!
* 如:
* 第一次合并:tlp = 0, right = 1
* 第二次合并:tlp = 2, right = 3
* 第三次合并:tlp = 0, right = 3
* ...
*/
tp = 0;
int tlp = left;
// System.out.println("进行拷贝:tlp = " + tlp + "; right = " + right);
while (tlp <= right) {
array[tlp++] = tmp[tp++];
}
// System.out.println();
}
max
。counts
,其长度是max加1
,其元素默认值都为0
。counts数组
的下标,以原数组中的元素出现次数作为counts数组
中该下标的元素值。 /**
* 计数排序
* 1. 找出待排序数组中元素值最大的,记为max。
* 2. 创建一个新数组counts,其长度是max加1,其元素默认值都为0。
* 3. 遍历原数组中的元素,以原数组中的元素作为counts数组的下标,以原数组中的元素出现次数作为counts数组中该下标的元素值。
* 4. 创建一个用来指定待排序数组索引的元素index。
* 5. 遍历counts数组,找出其中元素值大于0的元素,将其对应的索引作为元素值通过index填充到待排序数组中去,每处理一次,
* count中的该元素值减1,直到该元素值不大于0,依次处理counts中剩下的元素。
* 6. counts数组遍历处理完成后,排序就完成了
*
* @param source
*/
public static void countSort(int[] source) {
int max = 0; // 待排序数组的最大值
// TODO 获取待排序数组的最大元素值
for (int i = 0; i < source.length; i++) {
if (max < source[i]) {
max = source[i];
}
}
// TODO 创建用来计数的数组,长度为 待排序数组的最大值+1
int[] counts = new int[max + 1];
// TODO 遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。
for (int i = 0; i < source.length; i++) {
counts[source[i]]++;
}
// 创建一个用来指定待排序数组索引的元素
int index = 0;
for (int i = 0; i < counts.length; i++) { // 依次遍历counts数组
for (int count = counts[i]; count > 0; count--) { // 通过当前的count的个数来赋值到待排序的数组,实现排序
source[index++] = i;
}
}
}
对计数排序的优化,传统的计数排序有一个缺陷,那就是存在空间浪费的问题。比如一组数据{101,109,108,102,110,107,103}
,其中最大值为110
,按照原先的思路,我们需要创建一个长度为111的计数数组
,但是我们可以发现,它前面的[0,100]的空间完全浪费了,那怎样优化呢?
解决方案:将数组长度定为max-min+1
,即不仅要找出最大值,还要找出最小值,根据两者的差来确定计数数组的长度。
代码如下:
/** 对计数排序的优化,传统的计数排序有一个缺陷,那就是存在空间浪费的问题。比如一组数
* 据{101,109,108,102,110,107,103},其中最大值为110,按照原先的思路,我们需要创建一个长度
* 为111的计数数组,但是我们可以发现,它前面的[0,100]的空间完全浪费了,那怎样优化呢?
*
* 将数组长度定为max-min+1,即不仅要找出最大值,还要找出最小值,根据两者的差来确定计数数组的长度。
*
* @param source
*/
public static void countSortOptimization(int[] source) {
int max = 0; // 待排序数组的最大值
int min = 0; // 待排序数组的最小值
// TODO 获取待排序数组的最大元素值和最小元素值
for (int i = 0; i < source.length; i++) {
if (max < source[i]) {
max = source[i];
}
if(min > source[i]) {
min = source[i];
}
}
// TODO 创建用来计数的数组,长度为 待排序数组的最大值+1
int[] counts = new int[max - min + 1];
// TODO 遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。
for (int i = 0; i < source.length; i++) {
counts[source[i]]++;
}
// TODO 实现排序
int index = 0; // 创建一个用来指定待排序数组索引的元素
for (int i = 0; i < counts.length; i++) { // 依次遍历counts数组
for (int count = counts[i]; count > 0; count--) { // 通过当前的count的个数来赋值到待排序的数组,实现排序
source[index++] = i;
}
}
}
假设输入数据服从均匀分布,将数据分到有限数量的桶里,然后再对每个桶分别排序,最后把全部桶的数据合并,桶排序的时间复杂度取决于各个桶之间数据进行排序的时间复杂度,因为其他部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少,但相应的空间消耗就会增大,总结:划分多个范围相同的区间,每个子区间自排序,最后合并。
注意事项:
/**
* 桶排序:假设输入数据服从均匀分布,将数据分到有限数量的桶里,然后再对每个桶分别排序,最后
* 把全部桶的数据合并,桶排序的时间复杂度取决于各个桶之间数据进行排序的时间复杂度,因为其他
* 部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会
* 越少,但相应的空间消耗就会增大
*
* 思想:一句话总结:划分多个范围相同的区间,每个子区间自排序,最后合并。
*
* 注意事项:
* 1. 桶排序是计数排序的扩展版本,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储
* 一定范围的元素,通过映射函数,将待排序数组中的元素映射到各个对应的桶中,对每个桶中的元素
* 进行排序,最后将非空桶中的元素逐个放入原序列中。
* 2. 桶排序需要尽量保证元素分散均匀,否则当所有数据集中在同一个桶中时,桶排序失效。
*
* @param source
*/
public static void bucketSort(int[] source) {
int max = 0; // 待排序数组的最大值
int min = 0; // 待排序数组的最小值
// TODO 获取待排序数组的最大元素值和最小元素值
for (int i = 0; i < source.length; i++) {
if (max < source[i]) {
max = source[i];
}
if(min > source[i]) {
min = source[i];
}
}
// TODO 通过待排序数组的最大元素值和最小元素值 计算桶的数量
int bucketCount = (max - min) / source.length + 1;
ArrayList<ArrayList<Integer>> buckets = new ArrayList<>(); // 创建bucketCount个桶,每个桶使用TreeSet集合(该集合是按照大小存放元素的,因此放入桶内的元素就都是有序的)
for (int i = 0; i < bucketCount; i++) { // 初始化每个桶
buckets.add(new ArrayList<Integer>());
}
// TODO 将每个元素放入桶中
for(int i = 0; i < source.length; i++) {
int bucketNum = (source[i] - min) / source.length; // 计算当前元素应该放入哪个桶
buckets.get(bucketNum).add(source[i]); // 将当前元素放入指定桶
}
// TODO 对每个桶进行排序,这里我们的桶使用的是ArrayList集合,所以使用Collections类的sort()方法
for (int bucketNum = 0; bucketNum < bucketCount; bucketNum++) {
Collections.sort(buckets.get(bucketNum));
}
// TODO 将每个桶的元素取出,然后赋值到原先待排序的数组上,实现排序
int index = 0; // 创建一个用来指定待排序数组索引的元素
for (int bucketNum = 0; bucketNum < bucketCount; bucketNum++) {
for (int bucketPointer = 0; bucketPointer < buckets.get(bucketNum).size(); bucketPointer++) {
source[index++] = buckets.get(bucketNum).get(bucketPointer);
}
}
}
须知:
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序,这样一直到最高位排序完成以后,数列就变成了一个有序序列;
我们还需要知道:
/**基数排序:
* 基数排序属于"分配式排序",又称为"桶子法",顾名思义,它是通过键值的各个为的值,
* 将要排序的元素分配至某些"桶"中,达到排序的作用;
* 基数排序是属于稳定性的排序,是高效率的稳定性排序法;
* 基数排序是桶排序的扩展;
* 基数排序是这样实现的:将整数按位数切割成不同的数字,然后按照各个位数分别比较;
*
* 基本思想:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进
* 行一次排序,这样一直到最高位排序完成以后,数列就变成了一个有序序列;
*
* 基数排序的说明:
* 1. 基数排序是对传统桶排序的扩展,速度很快;
* 2. 基数排序是经典的空间换时间的方式,占用内存很大,当对海量数据排序时,容易造
* 成OutOfMemoryError错误;
* 3. 基数排序是稳定的(什么是稳定的?假定在待排序的记录序列中,存在很多具有相同的关键字的记录,
* 若经过排序,这些记录的相对次序不变,即在原序列中,r[i] = r[j],且r[i]在r[j]之前,而在排序后
* 的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的,否则称为不稳定的);
* 4. 有负数的数组,我们不用基数排序来进行排序
*
* @param source
*/
public static void baseSort(int[] source) {
// TODO 先得到待排序数组的最大的元素
int max = source[0]; // 假设数组的第一个元素最大
for (int i = 0; i < source.length; i++) {
if(max < source[i]) {
max = source[i];
}
}
int maxLength = (max + "").length(); // 获得最大元素的位数
/* TODO 定义一个二维数组,来表示10个桶,每个桶就是一个一维数组;
* 需要注意:
* 1. 二维数组包含10个一维数组(即10个桶)
* 2. 为了防止给桶放入数据溢出,每个桶的大小需要定为 待排序数组的长度
* 3. 基数排序是使用空间换时间的经典算法
*/
int[][] buckets = new int[10][source.length];
/* TODO 为了能够记录每个桶中实际存放了多少个元素,还需要定义一个一维数组来记录各个桶每轮
* 排序存放元素的个数;
*/
int[] bucketsInfo = new int[10];
// TODO 进行 maxLength轮排序,digit用来配合计算出本轮排序所使用位数的值
for (int n = 0, digit = 1; n < maxLength; n++, digit *= 10) {
// TODO 开始遍历 本轮待排序的数组
for (int i = 0; i < source.length; i++) {
int digitValue = source[i] / digit % 10; // 获取当前轮排序的 当前元素的对应位数值(即桶的序号)
// TODO 通过当前轮排序的位数值 放入到 对应的桶中
buckets[digitValue][bucketsInfo[digitValue]] = source[i]; // 放到对应的桶
bucketsInfo[digitValue]++; // 将标识该桶的元素个数的变量+1
}
int index = 0; // 用来在给待排序数组赋值时使用
// TODO 遍历每一个桶,并将同种的数据依次放到待排序的数组
for (int bucketNum = 0; bucketNum < bucketsInfo.length; bucketNum++) {
if(bucketsInfo[bucketNum] > 0) { // 如果当前桶有元素
for (int i = 0; i < bucketsInfo[bucketNum]; i++) { // 遍历桶内的元素
source[index++] = buckets[bucketNum][i]; // 将桶内的数据取出,放到待排序的数组
}
}
// TODO 程序执行到此,说明本桶已经遍历赋值完成,因此需要将存储本桶排序的信息清空
bucketsInfo[bucketNum] = 0;
}
// System.out.println("第" + (n + 1) + "轮桶排");
}
}
须知:
最坏,最好,平均时间复杂度均为O(nlogn)
,它也是不稳定排序
。可以从思想看出,堆排序过程中,会将每次的堆顶元素与待排序序列最后位置的元素交换,形成有序序列【将已经排序好的元素依次放到从数组最后到前的位置】,待排序的元素就始终在待排序序列之前;因此待排序的元素个数逐个减少,最后得到一个有序序列
/**
* 基本介绍:
* 1. 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复
* 杂度均为O(nlogn),它也是不稳定排序。
* 2. 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于(小于或等于)其左右孩子结点的值,称为大根堆(小根堆),
* 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。
*
* 须知:每个结点的值都小于或等于其左右孩子结点的值,称为小根堆;每个结点的值都大于或等于其左右孩子结点的值,称为大根堆
*
* 堆排序,基本思想:
* 1. 先将待排序序列(n个元素)构造成一个大根堆(升序使用,如果降序就构造小根堆),以数组体现
* 2. 此时,整个序列的最大值就是堆顶的根节点(小根堆时,整个序列的最小值就是堆顶的根节点)
* 3. 将堆顶元素[数组的第一个元素]与末尾元素[数组的最后一个元素]进行交换
* 4. 然后将剩余的n-1个元素[0到n-1]重新构造成一个大根堆(降序就构造小根堆),此时就会得到待排序序列的
* 次小值,再次执行第3步骤,然后第4步(反复执行,共反复执行n-1次),就能得到一个有序序列了;
*
* 可以从思想看出,堆排序过程中,会将每次的堆顶元素与待排序序列最后位置的元素交换,形成有序序列【将已经
* 排序好的元素依次放到从数组最后到前的位置】,待排序的元素就始终在待排序序列之前;因此待排序的元素个数
* 逐个减少,最后得到一个有序序列
*
* 编码思路:
* 1. 将无序序列构建成一个堆(根据升序/降序需求构建大根堆/小根堆)
* 2. 将堆顶元素(数组第一个元素)与末尾元素(数组待排序区域的最后一个元素)交换(将最大/最小元素"沉"到数组末端【有序区域的第一个位置】);
* 3. 重新调整结构为大根堆/小根堆,再执行2步骤,在执行3步骤(反复执行n-1次,最终得到一个有序序列)
*
* @param source
*/
public static void heapSort(int[] source) {
/* TODO 先将整个待排序的数组(对应的二叉树)构建成一个堆(根据升序/降序需求构建大根堆/小根堆)
* for循环的含义,从下到上、从右到左的将所有的非叶子节点对应的子树构建堆
* int i = source.length / 2 - 1:表示当前节点是非叶子节点
*/
for (int i = source.length / 2 - 1; i >= 0; i--) {
adjustHeap(source, i, source.length);
}
/* TODO 循环执行如下两步 数组长度-1次
* 将堆顶元素(数组第一个元素)与末尾元素(数组待排序区域的最后一个元素)交换(将最大/最小元素"沉"到数组末端【有序区域的第一个位置】);
* 重新调整结构为大根堆/小根堆,再执行2步骤,在执行3步骤(反复执行n-1次,最终得到一个有序序列);
*/
for (int j = source.length - 1; j > 0; j--) {
// 将堆顶元素(数组第一个元素)与末尾元素(数组待排序区域的最后一个元素)交换(将最大/最小元素"沉"到数组末端【有序区域的第一个位置】);
int tmp = source[j];
source[j] = source[0];
source[0] = tmp;
/*调整当前子树为大根堆(但是这里我们直接从0开始,表示调整整个数组(二叉树),因为编码中我们如果在指定位置进行调整子树为对应的是非
* 常麻烦的,所以我们直接调整整个数组(二次树),结果还是一样的)
*/
adjustHeap(source, 0, j);
}
}
/**
* 该方法将noleafNodeIndex对应的非叶子节点的子树调整为大根堆
* @param array 待调整的数组(整个二叉树)
* @param noleafNodeIndex 本次处理的非叶子节点在数组中的下标
* @param boundary 本次处理数组的边界值(即待排序区域在数组中的大小),该方法只会处理 [0, boundary)索引的元素
*/
private static void adjustHeap(int[] array, int noleafNodeIndex, int boundary) {
int subRootValue = array[noleafNodeIndex]; // 保存当前子树的根节点的值
/* TODO 开始调整
* int childIndex = noleafNodeIndex * 2 + 1:表示当前子树的 根节点的左子节点
* childIndex < boundary:表示在待处理的边界值之内(待排序的边界值之内)
* childIndex = childIndex * 2 + 1:表示 以当前子树的根节点的左子节点 为根节点的子树的左子节点 (即当前子树的左子树的左子节点)
*/
for (int childIndex = noleafNodeIndex * 2 + 1; childIndex < boundary; childIndex = childIndex * 2 + 1) {
if (childIndex + 1 < boundary && // 在左孩子节点+1(右孩子节点)没有超过待处理的边界值的情况下
array[childIndex] < array[childIndex + 1]) { // 如果左子节点的值小于右子节点的值(大根堆使用)
// array[childIndex] > array[childIndex + 1]) { // 如果左子节点的值大于右子节点的值(小根堆使用)
childIndex++; // 让当前的孩子节点指向右子节点
}
// 程序执行到此,子节点指向的就是左右子节点中最大的那个节点了(大根堆使用)
if (array[childIndex] > subRootValue){ // 如果子节点的值 大于 当前子树的根节点的值
array[noleafNodeIndex] = array[childIndex]; //将当前子树的根节点的值替换为子节点的值
noleafNodeIndex = childIndex; // 将当前子节点赋值到noleafNodeIndex,参与下一次的循环
} else { // 否则,即当前子树的根节点值 大于 当前子树的子节点值(即当前子树的根节点值为最大的),并且当前子树一定是大根堆!!!
break;
}
/*
// 程序执行到此,子节点指向的就是左右子节点中最小的那个节点了(小根堆使用)
if (array[childIndex] < subRootValue) { // 如果子节点的值 小于 当前子树的根节点的值
array[noleafNodeIndex] = array[childIndex]; // 将当前子树的根节点的值替换为子节点的值
noleafNodeIndex = childIndex; // 将当前子节点 赋值到noleafNodeIndex,参与下一次的循环
} else { // 否则,即当前子树的根节点值 小于 当前子树的子节点值(即当前子树的根节点值为最小的),并且当前子树一定是小根堆!!!
break;
}
*/
}
// 程序执行到此,说明noleafNodeIndex对应的非叶子节点的子树 的根节点值为最大的,并且当前子树是大根堆(小根堆)
array[noleafNodeIndex] = subRootValue; /*需要注意此时noleafNodeIndex的值已经变成了进行替换的当前子树的最大值(最小值)位置的节
点索引,因为在上面的for循环中,如果满足当前子树根节点与子节点交换的规则,我们就将原先的noleafNodeIndex值变成了childIndex,所以
我们此时是将当前子树的根节点的值放到调整之后的位置*/
}
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | 不占用额外内存 | 稳定 |
选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不占用额外内存 | 不稳定 |
插入排序 | O(n2) | O(n) | O(n2) | O(1) | 不占用额外内存 | 稳定 |
希尔排序 | O(n log n) | O(n log2 n) | O(n log2 n) | O(1) | 不占用额外内存 | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 占用额外内存 | 稳定 |
快速排序 | O(n log n) | O(n log n) | O(n2) | O(log n) | 不占用额外内存 | 不稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不占用额外内存 | 不稳定 |
计数排序 | O(n + k) | O(n + k) | O(n + k) | O(k) | 占用额外内存 | 稳定 |
桶排序 | O(n + k) | O(n + k) | O(n2) | O(n + k) | 占用额外内存 | 稳定 |
基数排序 | O(n * k) | O(n * k) | O(n * k) | O(n + k) | 占用额外内存 | 稳定 |
上表内的关键词解释:
不占用额外内存
和 占用额外内存
?