十大基础排序算法
冒泡排序
基本思路
对n个数进行排序,每次都是由前一个数跟后一个数比较,每循环一轮, 就可以将最大的数移到数组的最后, 总共循环n-1轮,完成对数组排序。动图演示
编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public static void sort(int[] arr) {
if (arr == null)
return;
int len = arr.length;
// i控制循环次数,长度为len的数组只需要循环len-1次,i的起始值为0所以i<len-1
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < len - i - 1; j++) {
// 如果前一个数比后一个数大,则交换位置将大的数往后放。
if (arr[j] > arr[j + 1]) {
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
}选择排序
基本思路
选择排序可以说是冒泡排序的改良版,通过循环对比,记录最小的数字下标与目标互换 这样相对于冒泡排序来说,比较的次数并没有改变,但是数据交换的次数大大减少。动图演示
- 编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public static void sort(int[] arr) {
if (arr == null)
return;
int len = arr.length;
// 用来保存每次比较后较小数的下标
int minIndex;
// i控制循环次数,长度为len的数组只需要循环len-1次,i的起始值为0所以i<len-1
for (int i = 0; i < len - 1; i++) {
minIndex = i;
//j控制比较次数,因为每次循环结束之后最小的数都已经放在了最前面,
//所以下一次循环的时候就可以跳过这个数,所以j的初始值为i+1而不需要每次循环都从0开始,并且j<len即可
for (int j = i + 1; j < len; j++) {
// 每比较一次都需要将较小数的下标记录下来
if (arr[minIndex] > arr[j]) {
minIndex = j;
}
}
// 当完成一次循环时,就需要将本次循环选取的最小数移动到本次循环开始的位置。
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
插入排序
基本思路
首先就默认数组中的第一个数的位置是正确的,即已经排序。然后取下一个数,与已经排序的数按从后向前的顺序依次比较, 如果该数比当前位置排好序的数小,则将排好序的数的位置向后移一位。 重复上一步骤,直到找到合适的位置。找到位置后就结束比较的循环,将该数放到相应的位置。动图演示
- 编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public static void sort(int[] arr) {
if (arr == null)
return;
int len = arr.length;
// target用来记录即将要排序的那个数的值即目标值
int target;
// index用来记录要交换的那个值的下标
int index;
// i控制循环次数,因为已经默认第一个数的位置是正确的,所以i的起始值为1,i<len,循环len-1次
for (int i = 1; i < len; i++) {
target = arr[i];
index = i;
for (int j = i; j > 0; j--) {
// 如果前一个数大于target,则设值成后一个数,并记录前一个数下标
if (target < arr[j - 1]) {
arr[j] = arr[j - 1];
index = j - 1;
}
}
// 更目标数的位置。
arr[index] = target;
}
}
希尔排序
基本思路
希尔排序也称为”缩小增量排序”,原理是先将需要排的数组分成多个子序列,这样每个子序列的元素个数就很少,再分别对每个对子序列进行插入排序。在该数组基本有序后 再进行一次直接插入排序就能完成对整个数组的排序。所以,要采用跳跃分割的策略。这里引入“增量”的概念,将相距某个增量的记录两两组合成一个子序列,然后对每个子序列进行直接插入排序, 这样得到的结果才会使基本有序(即小的在前边,大的在后边,不大不小的在中间)。希尔排序就是 直接插入排序的升级版。动图演示
- 编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public static void sort(int[] arr) {
if (arr == null)
return;
// 数组的长度
int len = arr.length;
// 初始的增量为数组长度的一半
int k = len / 2;
// while循环控制按增量的值来划不同分子序列,每完成一次增量就减少为原来的一半
// 增量的最小值为1,即最后一次对整个数组做直接插入排序
while (k > 0) {
// 里边其实就是升级版的直接插入排序了,是对每一个子序列进行直接插入排序,
// 所以直接将直接插入排序中的‘1’变为‘k’就可以了。
for (int i = k; i < len; i++) {
int j = i;
int target = arr[i];
while (j >= k && target < arr[j - k]) {
arr[j] = arr[j - k];
j -= k;
}
arr[j] = target;
}
k /= 2;
}
}
归并排序
基本思路
总体概括就是从上到下递归拆分,然后从下到上逐步合并。动图演示
- 编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57/**
* 递归拆分
*
* @param arr 待拆分数组
* @param left 待拆分数组最小下标
* @param right 待拆分数组最大下标
*/
public static void mergeSort(int[] arr, int left, int right) {
// 中间下标
int mid = (left + right) / 2;
if (left < right) {
// 递归拆分左边
mergeSort(arr, left, mid);
// 递归拆分右边
mergeSort(arr, mid + 1, right);
// 合并左右
sort(arr, left, mid, right);
}
}
/**
* 合并两个有序子序列
*
* @param arr 待合并数组
* @param left 待合并数组最小下标
* @param mid 待合并数组中间下标
* @param right 待合并数组最大下标
*/
public static void sort(int[] arr, int left, int mid, int right) {
// 临时数组,用来保存每次合并年之后的结果
int[] temp = new int[right - left + 1];
int i = left;
int j = mid + 1;
// 临时数组的初始下标
int k = 0;
// 这个while循环能够初步筛选出待合并的了两个子序列中的较小数
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 将左边序列中剩余的数放入临时数组
while (i <= mid) {
temp[k++] = arr[i++];
}
// 将右边序列中剩余的数放入临时数组
while (j <= right) {
temp[k++] = arr[j++];
}
// 将临时数组中的元素位置对应到真真实的数组中
for (int m = 0; m < temp.length; m++) {
arr[m + left] = temp[m];
}
}
快速排序
基本思路
快速排序也采用了分治的策略,这里引入了‘基准数’的概念。- 找一个基准数(一般将待排序的数组的第一个数作为基准数)
- 对数组进行分区,将小于等于基准数的全部放在左边,大于基准数的全部放在右边。
- 重复1,2步骤,分别对左右两个子分区进行分区,一直到各分区只有一个数为止。
动图演示
- 编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54/**
* 分区过程
*
* @param arr 待分区数组
* @param left 待分区数组最小下标
* @param right 待分区数组最大下标
*/
public static void quickSort(int[] arr, int left, int right) {
if (left < right) {
int temp = qSort(arr, left, right);
quickSort(arr, left, temp - 1);
quickSort(arr, temp + 1, right);
}
}
/**
* 排序过程
*
* @param arr 待排序数组
* @param left 待排序数组最小下标
* @param right 待排序数组最大下标
* @return 排好序之后基准数的位置下标,方便下次的分区
*/
public static int qSort(int[] arr, int left, int right) {
// 定义基准数,默认为数组的第一个元素
int temp = arr[left];
// 循环执行的条件
while (left < right) {
// 因为默认的基准数是在最左边,所以首先从右边开始比较进入while循环的判断条件
// 如果当前arr[right]比基准数大,则直接将右指针左移一位,当然还要保证left<right
while (left < right && arr[right] > temp) {
right--;
}
// 跳出循环说明当前的arr[right]比基准数要小,那么直接将当前数移动到基准数所在的位置,并且左指针向右移一位(left++)
// 这时当前数(arr[right])所在的位置空出,需要从左边找一个比基准数大的数来填充。
if (left < right) {
arr[left++] = arr[right];
}
// 下面的步骤是为了在左边找到比基准数大的数填充到right的位置。
// 因为现在需要填充的位置在右边,所以左边的指针移动,如果arr[left]小于或者等于基准数,则直接将左指针右移一位
while (left < right && arr[left] <= temp) {
left++;
}
// 跳出上一个循环说明当前的arr[left]的值大于基准数,需要将该值填充到右边空出的位置,然后当前位置空出。
if (left < right) {
arr[right--] = arr[left];
}
}
// 当循环结束说明左指针和右指针已经相遇。并且相遇的位置是一个空出的位置,
// 这时候将基准数填入该位置,并返回该位置的下标,为分区做准备。
arr[left] = temp;
return left;
}
堆排序
基本思路
堆是一种特殊的完全二叉树,分为大顶堆和小顶堆。大顶堆:每个结点的值都大于它的左右子结点的值,升序排序用大顶堆。小顶堆:每个结点的值都小于它的左右子结点的值,降序排序用小顶堆。
所以,需要先将待排序数组构造成大顶堆的格式,这时候该堆的顶结点就是最大的数,将其与堆的最后一个结点的元素交换。再将剩余的树重新调整成堆,再次首节点与尾结点交换,重复执行直到只剩下最后一个结点完成排序。动图演示
- 编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56public static void heapSort(int[] arr) {
if (arr == null) {
return;
}
int len = arr.length;
// 初始化大顶堆(从最后一个非叶节点开始,从左到右,由下到上)
for (int i = len / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, len);
}
// 将顶节点和最后一个节点互换位置,再将剩下的堆进行调整
for (int j = len - 1; j > 0; j--) {
swap(arr, 0, j);
adjustHeap(arr, 0, j);
}
}
/**
* 整理树让其变成堆
*
* @param arr 待整理的数组
* @param i 开始的结点
* @param j 数组的长度
*/
public static void adjustHeap(int[] arr, int i, int j) {
// 定义一个变量保存开始的结点
int temp = arr[i];
// k就是该结点的左子结点下标
for (int k = 2 * i + 1; k < j; k = 2 * k + 1) {
// 比较左右两个子结点的大小,k始终记录两者中较大值的下标
if (k + 1 < j && arr[k] < arr[k + 1]) {
k++;
}
// 经子结点中的较大值和当前的结点比较,比较结果的较大值放在当前结点位置
if (arr[k] > temp) {
arr[i] = arr[k];
i = k;
} else {
// 说明已经是大顶堆
break;
}
}
arr[i] = temp;
}
/**
* 交换数据
*
* @param arr 待整理的数组
* @param num1 开始的结点
* @param num2 交换的节点
*/
public static void swap(int[] arr, int num1, int num2) {
int temp = arr[num1];
arr[num1] = arr[num2];
arr[num2] = temp;
}
基数排序
基本思路
就是将待排序数据拆分成多个关键字进行排序,也就是说,基数排序的实质是多关键字排序。多关键字排序的思路是将待排数据里德排序关键字拆分成多个排序关键字;第1个排序关键字,第2个排序关键字,第3个排序关键字……然后,根据子关键字对待排序数据进行排序。动图演示
- 编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32public static void sort(int[] arr, int radix, int d) {
// 缓存数组
int[] tmp = new int[arr.length];
// buckets用于记录待排序元素的信息
// buckets数组定义了max-min个桶
int[] buckets = new int[radix];
for (int i = 0, rate = 1; i < d; i++) {
// 重置count数组,开始统计下一个关键字
java.util.Arrays.fill(buckets, 0);
// 将data中的元素完全复制到tmp数组中
System.arraycopy(arr, 0, tmp, 0, arr.length);
// 计算每个待排序数据的子关键字
for (int j = 0; j < arr.length; j++) {
int subKey = (tmp[j] / rate) % radix;
buckets[subKey]++;
}
for (int j = 1; j < radix; j++) {
buckets[j] = buckets[j] + buckets[j - 1];
}
// 按子关键字对指定的数据进行排序
for (int m = arr.length - 1; m >= 0; m--) {
int subKey = (tmp[m] / rate) % radix;
arr[--buckets[subKey]] = tmp[m];
}
rate *= radix;
}
}
计数排序
基本思路
计数排序采用了一种全新的思路,不再是通过比较来排序,而是将待排序数组中的最大值+1作为一个临时数组的长度,然后用临时数组记录待排序数组中每个元素出现的次数。最后再遍历临时数组,因为是升序,所以从前到后遍历,将临时数组中值>0的数的下标循环取出,依次放入待排序数组中,即可完成排序。计数排序的效率很高,但是实在牺牲内存的前提下,并且有着限制,那就是待排序数组的值必须 限制在一个确定的范围。动图演示
- 编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public static void sort(int[] arr) {
if (arr == null)
return;
int len = arr.length;
// 保存待排序数组中的最大值,目的是确定临时数组的长度(必须)
int maxNum = arr[0];
// 保存待排序数组中的最小值,目的是确定最终遍历临时数组时下标的初始值(非必需,只是这样可以加快速度,减少循环次数)
int minNum = arr[0];
// for循环就是为了找到待排序数组的最大值和最小值
for (int i = 1; i < len; i++) {
maxNum = maxNum > arr[i] ? maxNum : arr[i];
minNum = minNum < arr[i] ? minNum : arr[i];
}
// 创建一个临时数组
int[] temp = new int[maxNum + 1];
// for循环是为了记录待排序数组中每个元素出现的次数,并将该次数保存到临时数组中
for (int anArr : arr) {
temp[anArr]++;
}
// k=0用来记录待排序数组的下标
int k = 0;
// 遍历临时数组,重新为待排序数组赋值。
for (int i = minNum; i < temp.length; i++) {
while (temp[i] > 0) {
arr[k++] = i;
temp[i]--;
}
}
}
桶排序
基本思路
桶排序其实就是计数排序的强化版,需要利用一个映射函数首先定义有限个数个桶,然后将待排序数组内的元素按照函数映射的关系分别放入不同的桶里边,现在不同的桶里边的数据已经做了区分,比如A桶里的数要么全部大于B桶,要么全部小于B桶里的数。但是A,B桶各自里边的数还是乱序的。所以要借助其他排序方式(快速,插入,归并)分别对每一个元素个数大于一的桶里边的数据进行排序。最后再将桶里边的元素按照顺序依次放入待排序数组中即可。动图演示
无。。。。。编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35public static void sort(int[] arr) {
if (arr == null)
return;
int len = arr.length;
// 定义桶的个数,这里k的值要视情况而定,这里我们假设待排序数组里的数都是[0,100)之间的。
int k = 10;
// 用嵌套集合来模拟桶,外层集合表示桶,内层集合表示桶里边装的元素。
java.util.List<java.util.List<Integer>> bucket = new java.util.ArrayList<>();
// for循环初始化外层集合即初始化桶
for (int i = 0; i < k; i++) {
bucket.add(new java.util.ArrayList<>());
}
// 循环是为了将待排序数组中的元素通过映射函数分别放入不同的桶里边
for (int anArr : arr) {
bucket.get(anArr / 10).add(anArr);
}
// 这个循环是为了将所有的元素个数大于1的桶里边的数据进行排序。
for (int i = 0; i < k; i++) {
if (bucket.size() > 1) {
// 因为这里是用集合来模拟的桶所以用java写好的对集合排序的方法。
// 其实应该自己写一个方法来排序的。
java.util.Collections.sort(bucket.get(i));
}
}
// 将排好序的数重新放入待排序数组中
int m = 0;
for (java.util.List<Integer> list : bucket) {
if (list.size() > 0) {
for (Integer a : list) {
arr[m++] = a;
}
}
}
}