参考链接:
一、基本概念
冒泡排序和快排需要会手写代码,
堆排序和归并排序需要能够说出思想。
1、衡量排序算法的优劣:
- 时间复杂度 :分析关键字的比较次数和记录的移动次数(算法题要会计算且优化的东西)
- 空间复杂度: 分析排序算法中需要多少辅助内存
- 稳定性: 若两个记录 A 和 B 的关键字值相等,但排序后 A 、 B 的先后次序保持不变,则称这种排序算法是稳定的。
参考链接:
- 如何计算一个程序的时间和空间复杂度,讲解了时间复杂度,空间复杂度,最好、最坏时间复杂度,均摊时间复杂度。
- 稳定排序和不稳定排序,详细介绍了常用的排序方法是否为稳定排序,如何判断。
2、各种排序方法的性能比较
3、排序算法的选择
二、插入排序
定义: 每次将待排序元素,按照其关键字大小插入到已经排好序的子文件的适当位置,直到全部记录插入完为止。
1、直接插入排序
1.1 基本思想
把
n
n
n 个待排序的元素看成一个有序表和一个无序表,开始时有序表只包含一个元素,无序表中包含
n
−
1
n-1
n−1 个元素,每次遍历从无序表中取出一个元素从后往前依次与有序表中的元素进行比较,将它插入到有序表中的适当位置,使之称为新的有序表。
也就是每次遍历有序表多一个数,无序表少一个数,直至所有元素有序。
1.2 示意图(图源尚硅谷)
1.3 代码实现
public class DirectInsertSort {
public static void main(String[] args) {
int[] data = {9, -16, 21, 23, -30, -49, 21, 30, 30};
System.out.println("排序之前:\n" + java.util.Arrays.toString(data));
insertSort(data);
System.out.println("排序之后:\n" + java.util.Arrays.toString(data));
}
//直接插入排序
public static void insertSort(int[] arr) {
int j,temp;
for (int i = 1; i < arr.length; i++) {
temp = arr[i];//先把待插入的数据存起来
j = i;
//要用temp和前面已经排好序的有序数组进行比较,如果比前一个数小,前一个数后移
// 如果这里用的是arr[j] < arr[j - 1];循环只会进行一次
while (j > 0 && temp < arr[j - 1]) {
arr[j] = arr[j - 1];
j--;//这里arr[j]指向的位置是空的,可以被覆盖的
}
//此时j指向的位置左边比它小,右边比它大,直接插入就可以
arr[j] = temp;
}
}
}
注意:
- 内层循环条件要用 j > 0 && temp < arr[j - 1],如果这里用的是 arr[j] < arr[j - 1]; 内层循环只会进行一次,是错误的!!!
2、希尔排序
2.1 基本思想
- 将数的个数设为 n,取奇数 k=n/2,将下标差值为k的数分为一组,构成有序序列。
- 再取 k=k/2 ,将下标差值为 k 的数分为一组,利用直接插入排序将该组数组变为有序序列。
- 重复第二步,直到 k=1 执行简单插入排序。
2.2 示意图
2.3 代码实现
public class ShellSort {
public static void main(String[] args) {
int[] data = {9, -16, 21, 23, -30, -49, 21, 30, 30};
System.out.println("排序之前:\n" + java.util.Arrays.toString(data));
ShellSort(data);
System.out.println("排序之后:\n" + java.util.Arrays.toString(data));
}
public static void ShellSort(int[] data) {
int arrayLength = data.length;
int h = 1;
//该区间按照三分法来实现
while (h <= arrayLength / 3) {
h = h * 3 + 1;
}
while (h > 0) {
//对每一个子分组进行简单插入排序,从后往前遍历
for (int i = h; i < arrayLength; i++) {
int temp = data[i];
int j = i - h;
for (; j >= 0 && data[j] - temp > 0; j -= h) {
data[j + h] = data[j];
}
data[j + h] = temp;
}
h = (h - 1) / 3;
}
}
}
三、交换排序
通过多次比较数组中两个数字的大小,不满足排序要求,则进行交换。
1、冒泡排序
1.1 基本思想
以升序排序为例,从前往后遍历数组,每次比较相邻元素的大小,逆序则交换,使得最大的元素逐渐移动到序列的末尾
n
n
n 位置,下一次遍历固定最后一个元素,将次大的元素放到
n
−
1
n -1
n−1的位置,依次遍历,直到整个数组有序。
优化: 排序的过程中,也有可能一趟下来元素没有进行过交换,代表此时整个序列是正序的,所以我们可以在交换元素的位置设置一个 flag ,只要状态没有发生改变,代表这一趟没有进行过交换,可以结束程序了。
1.2 示意图
1.3 代码实现
public class BubbleSort {
public static void main(String[] args) {
int[] data = {9, -16, 21, 23, -30, -49, 21, 30, 30};
System.out.println("排序之前:\n" + java.util.Arrays.toString(data));
BubbleSort(data);
System.out.println("排序之后:\n" + java.util.Arrays.toString(data));
}
public static void BubbleSort(int[] a) {
//外层循环表示第几趟
for (int i = 0; i < a.length - 1; i++) {
//优化,设置是否发生过交换的flag
boolean flag = false;
//内层循环实现交换和遍历
for (int j = 0; j < a.length - 1 - i; j++) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
flag = true;
}
}
//一次遍历之后没有进行过交换
if (!flag){
break;
}
}
}
}
2、快速排序(分区交换排序)
2.1 基本思想(对冒泡的一种改进)
选取序列中的某个元素作为标准(一般取第一个元素),通过一次双指针遍历,先将首元素保存起来,然后右指针移动,找到一个比基准元素小的元素放置到左指针指向的位置,左指针向右移动,找到一个比基准元素大的元素放置到右指针指向的位置,继续移动右指针,直到左右指针相遇,将基准元素放到该位置。
此时将待排序的元素划分为两个子序列,左边序列都小于基准元素,右边序列都大于基准元素。
然后对两个子序列分别重复上述操作,直到每一个子序列只有一个元素为止,最后得到有序序列。(递归)
2.2 示意图
2.3 代码实现
public class QuickSort {
public static void main(String[] args) {
int[] data = {9, -16, 21, 23, -30, -49, 21, 30, 30};
System.out.println("排序之前:\n" + java.util.Arrays.toString(data));
QuickSort(data, 0, data.length - 1);
System.out.println("排序之后:\n" + java.util.Arrays.toString(data));
}
//左闭右闭区间
public static void QuickSort(int[] a, int start, int end) {
//递归终止条件:序列只有一个元素
if (start >= end) return;
int mark = a[start]; //暂存基准元素
int left = start; //指向序列的左边
int right = end; //指向序列的右边
while (left < right) {
//右指针移动,找比基准元素小的元素
while (left < right && a[right] >= mark) {
right--;
}
//左指针移动,找到比基准元素大的元素
while (left < right && a[left] <= mark) {
left++;
}
if (left < right) {
//交换两元素
int temp = a[left];
a[left] = a[right];
a[right] = temp;
}
}
//当 left == right 的时候跳出循环,将基准元素放置在指定位置上
int temp = a[right];
a[right] = a[start];
a[start] = temp;
//递归左序列和右序列
QuickSort(a, start, right - 1);
QuickSort(a, right + 1, end);
}
}
四、选择排序
将待排序的元素分为已排序(初始为空)和未排序两组,每次遍历将未排序的元素中的最小值放到已排序的组中。
1、简单选择排序
1.1 基本思想
一趟遍历,在未排序的数组中找到最小的元素,然后将它放到首位,每次遍历都固定了一个最小值(有点像冒泡排序,每次固定最大值一样),直到遍历结束。
1.2 示意图
1.3 代码实现
public class SelectSort {
public static void main(String[] args) {
int[] data = {9, -16, 21, 23, -30, -49, 21, 30, 30};
System.out.println("排序之前:\n" + java.util.Arrays.toString(data));
SelectSort(data);
System.out.println("排序之后:\n" + java.util.Arrays.toString(data));
}
//左闭右闭区间
public static void SelectSort(int[] a) {
int index = 0;//记录每次遍历找到的最小值的下标
//遍历整个数组找最小值
for (int i = 0; i < a.length; i++) {
index = i;//每次进来将指向最小值的指针重置到首位
for (int j = i;j < a.length;j++) {
if (a[j] < a[index]) {
index = j;//更新最小下标
}
}
if (index != i) {
//交换最小的和第一个元素
int temp = a[i];
a[i] = a[index];
a[index] = temp;
}
}
}
}
2、堆排序
2.1 基本思想
堆:分为最大堆和最小堆,堆中的每一个节点值都大于等于(或小于等于)子树中所有节点的值。或者说,任意一个节点的值都大于等于(或小于等于)所有子节点的值。(堆可以近似的看成完全二叉树)
可以参考:二叉树和堆的知识点整理,这里排序算法的堆实现是基于数组的实现。(堆还可以基于链表实现)
堆排序过程(升序排序):将无序序列创建为一个大顶堆,之后取堆顶元素(即序列中的最大值),然后将堆重建一下之后再取堆顶元素(次大值),如此遍历,直到取出所有元素。
2.2 示意图
2.3 代码实现
public class HeapSort {
public static void heapSort(int[] data) {
int arrayLength = data.length;
// 循环建堆
for (int i = 0; i < arrayLength - 1; i++) {
// 建堆
buildMaxdHeap(data, arrayLength - 1 - i);
// 交换堆顶和最后一个元素
swap(data, 0, arrayLength - 1 - i);
}
}
// 对data数组从0到lastIndex建大顶堆
private static void buildMaxdHeap(int[] data, int lastIndex) {
// 从lastIndex处节点(最后一个节点)的父节点开始,后序遍历二叉树
for (int i = (lastIndex - 1) / 2; i >= 0; i--) {
// k保存当前正在判断的节点
int k = i;
// 如果当前k节点的子节点存在
while (k * 2 + 1 <= lastIndex) {
// k节点的左子节点的索引,右子节点为 2 * k + 2;
int biggerIndex = 2 * k + 1;
// 如果biggerIndex小于lastIndex,即biggerIndex +1,代表k节点的右子节点存在
if (biggerIndex < lastIndex) {
// 如果右子节点的值较大
if (data[biggerIndex] - data[biggerIndex + 1] < 0) {
// biggerIndex总是记录较大子节点的索引
biggerIndex++;
}
}
// 如果k节点的值小于其较大子节点的值
if (data[k] - data[biggerIndex] < 0) {
// 交换它们
swap(data, k, biggerIndex);
// 将biggerIndex赋给k,开始while循环的下一次循环
// 重新保证k节点的值大于其左、右节点的值
k = biggerIndex;
} else {
break;
}
}
}
}
// 交换data数组中i、j两个索引处的元素
private static void swap(int[] data, int i, int j) {
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
public static void main(String[] args) {
int[] data = {9, -16, 21, 23, -30, -49, 21, 30, 30};
System.out.println("排序之前:\n" + java.util.Arrays.toString(data));
heapSort(data);
System.out.println("排序之后:\n" + java.util.Arrays.toString(data));
}
}
五、归并排序
1、归并排序
1.1 基本思想
将两个有序表合并成一个有序表
二路归并:也就是初始将元素进行切分,切到二叉树最低端每个数组只包含一个元素的时候,开始进行合并,两个小数组合并成一个有序的大数组,然后这两个有序的大数组再合并成更大的数组,直到数组整个合并完毕。
1.2 示意图
1.3 代码实现
public class MergeSort {
public static void mergeSort(int[] data) {
// 归并排序
sort(data, 0, data.length - 1);
}
// 将索引从left到right范围的数组元素进行归并排序
private static void sort(int[] data, int left, int right) {
if (left < right) {
//找出中间索引,切分数组,直到数组只剩一个元素,左闭右闭区间
int center = left + (right - left) / 2;
sort(data, left, center);
sort(data, center + 1, right);
//合并
merge(data, left, center, right);
}
}
// 将两个数组进行归并,归并前两个数组已经有序,归并后依然有序
private static void merge(int[] data, int left, int center, int right) {
int[] tempArr = new int[data.length];
int mid = center + 1; //右边数组的起始位置
int third = left; //临时数组的起始位置
int temp = left; //暂存,之后数组复制要用
while (left <= center && mid <= right) {
if (data[left] - data[mid] <= 0) {//左边数组的元素小,先放到临时数组中
tempArr[third++] = data[left++];
} else {//右边数组的数字小,放到临时数组中
tempArr[third++] = data[mid++];
}
}
//右边数组多出来的部分,这里多出来的都是排好序的且比另外一部分元素大的元素
while (mid <= right) {
tempArr[third++] = data[mid++];
}
//左边数组多出来的部分
while (left <= center) {
tempArr[third++] = data[left++];
}
//临时数组复制到主数组中
while (temp <= right) {
data[temp] = tempArr[temp++];
}
}
public static void main(String[] args) {
int[] data = {9, -16, 21, 23, -30, -49, 21, 30, 30};
System.out.println("排序之前:\n" + java.util.Arrays.toString(data));
mergeSort(data);
System.out.println("排序之后:\n" + java.util.Arrays.toString(data));
}
}
2、基数排序
2.1 基本思想
将所有待比较的数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序,这样从最低位排序一直到最高位排序完成以后,数列就变为了一个有序序列。
参考链接:程序员那些必须掌握的排序算法(下)