文章目录
算法与数据结构 — 排序算法,含 Java 高质量算法实现
一、概念
- 稳定性:
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
二、排序算法分类
三、时间复杂度
排序算法 | (平均)时间复杂度 | (最好)时间复杂度 | (最坏)时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
比较排序: | |||||
插入排序 | O(n2) | O(n) (基本有序) | n2 | O(1) | 稳定 |
希尔排序 | 不稳定 | ||||
选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n2) | O(nlogn) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
非比较排序: | |||||
计数排序 | O(n + k) | O(n + k) | O(n + k) | O(n + k) | 稳定 |
桶排序 | O(n + k) | O(n) | O(n2) | O(n + k) | 稳定 |
基数排序 | O(n * k) | O(n * k) | O(n * k) | O(n + k) | 稳定 |
四、排序算法详解
1. 直接插入排序
-
算法思想:
第 i 趟插入排序为:在含有 i − 1个元素的 有序子序列 中插入一个元素,使之成为含有 i 个元素的有序子序列。在查找插入位置的过程中,同时后移元素,所以适合从后向前扫描。
整个过程为进行 n − 1 趟插入, 即先将整个序列的第 1个元素看成是有序的,然后从第 2个元素起,逐个进行插入,直到整个序列有序 为止。 -
算法实现:
/** * 直接插入排序算法实现 O(n2) * 在含有 i − 1个元素的有序子序列中插入一个元素,使之成为含有 i 个元素的有序子序列 * * @param arr 待排序序列 * @return */ public static int[] insertSort(int[] arr) { // 假定第一个元素被放到了正确的位置上 for (int i = 1; i < arr.length; i++) { // 待排序元素 int target = arr[i]; int preIndex = i - 1; while (preIndex >= 0 && arr[preIndex] > target) { // 后移元素 arr[preIndex + 1] = arr[preIndex]; preIndex--; } // 找到插入位置之后,将第 i 个元素插入到有序序列中 arr[preIndex + 1] = target; } return arr; }
-
算法优化:
注意到插入排序的过程中,每次是将第 i 个元素插入到前面有序序列中,既然是插入到有序序列中,那么可以采用二分查找的思想来寻找第i个元素插入到前面有序序列中的位置,这样优化之后的算法时间复杂度为O(nlogn)。优化之后的算法:
public int[] insertSortV2(int[] nums) { for (int i = 1; i < nums.length; i++) { int current = nums[i]; // 采用二分查找,查找插入排序第i个元素在前面有序序列中的位置 pivot int low = 0, mid = 0, high = i - 1; int pivot = 0; while (low <= high) { mid = (low + high) / 2; if (current < nums[mid]) { high = mid - 1; } if (current >= nums[mid]) { low = mid + 1; } } // 找到插入位置之后,将第 i 个元素插入到有序序列中 pivot = low; for (int j = i; j > pivot; j--) { nums[j] = nums[j - 1]; } nums[pivot] = current; } return nums; }
-
测试平台:LeetCode:912. 排序数组
1是使用最原始的插入排序的算法,2是使用优化之后的插入排序的算法
2. 希尔排序
-
算法思想:
希尔排序的思想是采用插入排序的方法,先让数组中任意间隔为 h 的元素有序,刚开始 h 的大小可以是 h = n / 2,接着让 h = n / 4,让 h 一直缩小,当 h = 1 时,也就是此时数组中任意间隔为1的元素有序,此时的数组就是有序的了。 -
过程演示:
-
算法实现:
/** * 希尔排序算法实现 * * @param arr 待排序序列 * @return */ public static int[] shellSort(int[] arr) { int len = arr.length; // 设置默认增量为:n/2 int gap = len / 2; while (gap > 0) { for (int i = gap; i < len - 1; i++) { int current = arr[i]; int preIndex = i - gap; while (preIndex >= 0 && arr[preIndex] > current) { arr[preIndex + gap] = arr[preIndex]; preIndex -= gap; } arr[preIndex + gap] = current; } gap /= 2; } return arr; }
3. 选择排序
-
算法思想:
首先 在 整个排序序列 中找到最小(大)元素,存放到排序序列的 起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到 已排序序列的末尾 。以此类推,直到所有元素均排序完毕。 -
算法实现:
/** * 选择排序算法实现 O(n2) * 从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾已排序序列的末尾 * * @param arr 待排序序列 * @return */ public static int[] selectSort(int[] arr) { // min记录 每趟排序过程中 最小值的下标 int minIndex; for (int i = 0; i < arr.length; i++) { minIndex = i; for (int j = i + 1; j < arr.length; j++) { // 未排序序列中选择最小的元素 if (arr[j] < arr[minIndex]) { minIndex = j; } } // 一趟排序结束,选择最小的值放到已排序的第 i 的位置上 if (minIndex != i) { int temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; } } return arr; }
4. 堆排序
-
概念:
- 堆排序:
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序 - 堆:
堆是具有以下性质的 完全二叉树 :每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
构建的 (大/小) 顶堆,二叉树的根是整个序列的 最(大/小)值,堆排序正是利用的是这个特性。
- 堆排序:
-
算法思想:
- 步骤1:将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区
- 步骤2:将 堆顶元素R[1] 与 最后一个元素R[n] 交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 步骤3:由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1) 调整为新堆 ,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
-
算法实现:
5. 冒泡排序
-
算法思想:
首先将第 1个元素和第 2个元素进行比较,若前者大于后者,则两者交换位置,然后比较 第 2个元素和第 3个元素。依此类推,直到第 n − 1个元素和第 n个元素进行过比较或交换为止。上 述过程称为 一趟冒泡排序,其结果是使得 n个元素中值最大的那个元素被安排在最后一个元素的位置 上。然后进行第二趟排序,即对前 n − 1个元素进行同样的操作,使得前 n − 1个元素中值最大的那 个元素被安排在第 n − 1个位置上。
一般地,第 i 趟冒泡排序是从前 n − i + 1个元素中的第 1个元素 开始,两两比较,若前者大于后者,则交换,结果使得前 n − i + 1个元素中最大的元素被安排在第 n − i + 1个位置上。
-
优化:
显然,判断冒泡排序结束的条件是“在一趟排序中没有进行过交换元素的操作”, 为此,设立一个标志变量 flag,flag = 1表示有过交换元素的操作,flag = 0表示没有过交换元素的操 作,在每一趟排序开始前,将 flag置为 0,在排序过程中,只要有交换元素的操作,就及时将 flag置 为 1。因为至少要执行一趟排序操作,故第一趟排序时,flag = 1。 -
算法实现:
以下提供的两个函数,皆为冒泡算法的实现。
bubbleSort 为 原始的冒泡排序,bubbleSortV2为加入flag优化过之后的冒泡排序。
大家可以在第二个循环内加入count来测试以下优化前后两个算法的差异,可以感受到第二个函数循环执行的次数要少于第一个。/** * 冒泡排序算法实现 * * @param arr 待排序序列 * @return */ public static int[] bubbleSort(int[] arr) { int len = arr.length; for (int i = 0; i < len; i++) { for (int j = 0; j < len - 1 - i; j++) { if (arr[j + 1] < arr[j]) { int temp = arr[j + 1]; arr[j + 1] = arr[j]; arr[j] = temp; } } } return arr; } /** * 冒泡排序函数实现,通过判断一趟冒泡排序中,位置是否发生变化来终止 * * @param arr 待排序数组 * @return */ public static int[] bubbleSortV2(int[] arr) { // 判断一趟冒泡排序过程中,是否发生交换,如果没有发生交换,则代表序列已经有序。 // flag = 1:发生交换,flag = 0:无交换 int flag = 1; for (int i = 0; i < arr.length && flag == 1; i--) { flag = 0; for (int j = 0; j < arr.length - i - 1; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; flag = 1; } } } return arr; }
6. 快速排序
-
算法思想:
在序列中任意选择一个元素(通常称为分界元素或 基准 元素),把小于或等于基准的所有元素都移到基准的前面,把大于基准的所有元素都移到基准的后面, 这样,当前序列就被划分成前后两个子序列,其中前一个子序列中的所有元素都小于后一个子序列的所有元素,并且基准正好处于排序的最终位置上。然后分别对这两个子序列递归 地进行上述排序过程,直到所有元素都处于排序的最终位置上,排序结束。
快速排序的本质:每趟排序将选择的基准放到正确的位置。 -
算法实现:
/** * 快速排序:递归实现 * * @param arr * @param low * @param high */ public void quickSort(int[] arr, int low, int high) { if (low < high) { // 找寻基准数据的正确索引 int pivotIdx = getPivotIndex(arr, low, high); // 进行迭代对 基准之前和之后的数组进行相同的操作使整个数组变成有序 quickSort(arr, low, pivotIdx - 1); quickSort(arr, pivotIdx + 1, high); } } /** * 快速排序算法 -- 基准位置 * 一趟快排,将基准放到正确的位置,即基准左边的数都 <= 基准,即基准右边的数都 >= 基准 * * @param arr * @param low * @param high * @return */ public int getPivotIndex(int[] arr, int low, int high) { // 以序列中第一个元素作为基准数据 int pivot = arr[low]; while (low < high) { // 当队尾的元素大于等于基准数据时,向前挪动high指针 while (low < high && arr[high] >= pivot) { high--; } // 如果队尾元素小于基准,需要将其赋值给low arr[low] = arr[high]; // 当队首元素小于等于基准数据时,向前挪动low指针 while (low < high && arr[low] <= pivot) { low++; } // 当队首元素大于基准数据时,需要将其赋值给high arr[high] = arr[low]; } // 跳出循环时low和high相等,此时的low或high就是基准的正确索引位置 arr[low] = pivot; // 返回基准的正确位置 return low; }
7. 归并排序
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。
-
算法思想:
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。归并排序算法有两个基本的操作,一个是分,也就是把原数组划分成两个子数组的过程。另一个是治,它将两个有序数组合并成一个更大的有序数组。
将数组平均分成两部分: center = (left + right)/2,当数组分得足够小时—数组中只有一个元素时,只有一个元素的数组自然而然地就可以视为是有序的,此时就可以进行合并操作了。因此,上面讲的合并两个有序的子数组,是从 只有一个元素 的两个子数组开始合并的。
8. 计数排序
-
算法思想:
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为 键 存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是 有确定范围的整数 。当输入的元素是 n 个0到k之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
-
算法实现:
/** * 计数排序算法实现 * * @param arr * @return */ public static int[] countingSort(int[] arr) { if (arr.length == 0) { return arr; } // min,max:确定用于计数的数组的大小,计数数组大小为:(max - min + 1) int min = arr[0], max = arr[0]; for (int i = 1; i < arr.length; i++) { if (arr[i] < min) { min = arr[i]; } if (arr[i] > max) { max = arr[i]; } } int[] bucket = new int[max - min + 1]; // 统计序列中各个元素出现的次数count for (int i = 0; i < arr.length; i++) { int bias = arr[i] - min; // 第 i 个元素频次加1 bucket[bias]++; } // 把计数数组统计好的数据汇总到原数组 int index = 0; for (int i = 0; i < bucket.length; i++) { for (int j = 0; j < bucket[i]; j++) { arr[index] = min + i; index++; } } return arr; }
9. 桶排序
- 算法思想:
桶排序就是把最大值和最小值之间的数进行瓜分,例如分成 10 个区间,10个区间对应10个桶,我们把各元素放到对应区间的桶中去,再对每个桶中的数进行排序,可以采用归并排序,也可以采用快速排序之类的。
之后每个桶里面的数据就是有序的了,我们在进行合并汇总。 - 算法演示:
10. 基数排序
-
算法思想:
基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,时间复杂度为O(kn), n为数组长度,k为数组中的数的最大的位数;基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
-
算法实现:
/** * 基数排序算法实现 * * @param arr 待排序序列 * @return */ public static int[] radioSort(int[] arr) { if (arr == null || arr.length < 2) { return arr; } int n = arr.length; int max = arr[0]; // 找出最大值 for (int i = 1; i < n; i++) { max = Math.max(max, arr[i]); } // 计算最大值是几位数 int num = 1; while (max / 10 > 0) { num++; max = max / 10; } // 创建10个桶 ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(10); // 初始化桶 for (int i = 0; i < 10; i++) { bucketList.add(new LinkedList<Integer>()); } // 进行每一趟的排序,从个位数开始排 for (int i = 1; i <= num; i++) { for (int j = 0; j < n; j++) { // 获取每个数最后第 i 位是数组 int radio = (arr[j] / (int) Math.pow(10, i - 1)) % 10; //放进对应的桶里 bucketList.get(radio).add(arr[j]); } //合并放回原数组 int k = 0; for (int j = 0; j < 10; j++) { for (Integer t : bucketList.get(j)) { arr[k++] = t; } //取出来合并了之后把桶清光数据 bucketList.get(j).clear(); } } return arr; }