排序算法一插入&希尔&归并
1 排序算法简介
排序算法是将一组数据按照特定的顺序进行排列的算法。常见的排序算法包括快速排序、冒泡排序,希尔排序,二分排序(归并),桶排序,堆排序,基数排序,插入排序,选择排序等。不同的排序算法具有不同的时间复杂度和空间复杂度,适用于不同的场景。
2 排序算法评价指标
-
时间效率
-
空间复杂度
-
比较次数和交换次数
在排序算法中,比较次数和交换次数是两个重要的操作指标。几乎所有的排序算法都会涉及到比较操作,以确定元素的相对大小。而某些排序算法(如冒泡排序、选择排序等)则需要进行交换操作,以改变元素的相对位置。
- 稳定性
稳定性是排序算法的一个重要特性,它指的是在排序过程中,具有相同值的元素在排序后的相对位置是否保持不变。如果排序算法是稳定的,那么具有相同值的元素在排序后的相对位置与排序前相同。反之,如果排序算法是不稳定的,那么具有相同值的元素在排序后的相对位置可能会发生变化。
示例
对于给定的输入序列 1 9 3 5 3,我们有两种可能的排序结果:
- 第一种:1 3 3 5 9
- 第二种:1 3 3 5 9
从这两种结果可以看出,它们都是正确的排序结果。但是,如果我们考虑稳定性,那么第二种排序结果都是稳定的,因为两个值为 3
的元素在排序后的相对位置没有发生变化。
说明:快速排序通常被认为是不稳定的,而冒泡排序和插入排序则是稳定的。因此,在选择排序算法时,除了考虑时间效率和空间复杂度外,还需要考虑算法的稳定性是否满足实际需求。
3 稳定性的应用
问题描述,订单需要首先按照金额从小到大排序,当金额相同时,需要按照下单时间进行排序。当订单从订单中心传输过来时已经按照时间排好序时,我们需要考虑如何在不破坏时间顺序的前提下,按照金额进行排序。
排序需求
- 主要排序依据:订单金额(从小到大)
- 次要排序依据:下单时间(当金额相同时)
- 前提条件:订单已按时间排好序
排序算法选择
不稳定排序算法
- 缺点:如果不选择稳定的排序算法,那么在按照金额排序时,可能会破坏已经按照时间排好的顺序。这意味着在排序过程中,我们可能需要同时比较金额和下单时间两个字段,增加了算法的复杂性和执行时间。
稳定排序算法
- 优点:选择稳定的排序算法可以确保在按照金额排序时,不会破坏已经按照时间排好的顺序。这意味着我们只需要比较金额字段,当下金额相同时,由于订单已经按照时间排好序,所以它们的相对位置不会改变。
- 推荐算法:如插入排序、归并排序、冒泡排序(虽然效率较低但稳定)或基于比较的稳定排序算法变体。
4 插入排序简介
插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入,直到整个序列有序。
示例说明
打扑克。分成两部分:一部分是你手里的牌(已经排好序),一部分是要拿的牌(无序)。把一个无序的数列一个个插入到有序数列中。
插入排序的步骤如下:
- 从第一个元素开始,该元素可以认为已经被排序。
- 取出下一个元素,在已经排序的元素序列中从后向前扫描。
- 如果该元素(已排序)大于新元素,将该元素移到下一位置。
- 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置。
- 将新元素插入到该位置后。
- 重复步骤 2~5。
插入排序分析
- 时间复杂度 :O(N^2)
- 空间复杂度 :O(n)
- 稳定性 :稳定
看以下这个例子:对7 8 9 0 4 3进行插入排序
初始状态:
7 8 9 0 4 3
第一步:从第二个元素开始(索引为1),将其与前面的元素比较并插入到正确的位置。此时,7是已排序的序列,8是待插入的元素。
7 8 9 0 4 3 (没有变化,因为8 > 7)
第二步:继续向后,将9与前面的元素比较并插入到正确的位置。此时,7 8是已排序的序列,9是待插入的元素。
7 8 9 0 4 3 (没有变化,因为9 > 8)
第三步:继续向后,将0与前面的元素比较并插入到正确的位置。此时,7 8 9是已排序的序列,0是待插入的元素。
0 7 8 9 4 3
(注意,此时0被插入到了正确的位置,即序列的开头)
第四步:继续向后,将4与前面的元素比较并插入到正确的位置。此时,0 7 8 9是已排序的序列,4是待插入的元素。
0 4 7 8 9 3
(注意,此时4被插入到了0和7之间)
第五步:继续向后,将3与前面的元素比较并插入到正确的位置。此时,0 4 7 8 9是已排序的序列,3是待插入的元素。
0 3 4 7 8 9
(注意,此时3被插入到了0和4之间)
5 插入排序示例
/**
* 插入排序
* 1.从第一个元素开始,该元素可以认为已经被排序
* 2.取出下一个元素,在已经排序的元素序列中从后向前扫描
* 3.如果该元素(已排序)大于新元素,将该元素移到下一位置
* 4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
* 5.将新元素插入到该位置中
* 6.重复步骤2~5
*/
public class InsertionSort {
public static void main(String[] args)
{
InsertionSort insertionSort = new InsertionSort();
int[] nums = {3,2,1,5,4,10};
int[] res = insertionSort.insertionSort(nums);
insertionSort.print(res);
System.out.println();
int[] res2 = insertionSort.insertionSort2(nums);
insertionSort.print(res2);
}
/**
* 插入排序:升序
* @param nums
* @return
*/
public int[] insertionSort(int[] nums)
{
for (int i = 1; i < nums.length; i++) {
int temp = nums[i];
int j = i - 1;
for ( ; j >= 0 ; j--) {
if (temp < nums[j]){
nums[j + 1] = nums[j];
}else{
break;
}
}
nums[j + 1] = temp;
}
return nums;
}
/**
* 插入排序:降序
* @param nums
* @return
*/
public int[] insertionSort2(int[] nums)
{
for (int i = 1; i < nums.length; i++) {
int temp = nums[i];
int j = i - 1;
for (; j >= 0; j--) {
if (nums[j] < temp){
nums[j + 1] = nums[j];
}else{
break;
}
}
nums[j + 1] = temp;
}
return nums;
}
public void print(int[] nums) {
for (int i = 0; i < nums.length; i++) {
System.out.print(nums[i] + " ");
}
}
}
6 希尔排序简介
希尔排序(Shell Sort)是一种插入排序的改进版本,也被称为“缩小增量排序”(Diminishing Increment Sort)。
- 定义:希尔排序是基于插入排序的算法,通过比较相距一定间隔的元素来工作,这些间隔被称为“增量”。随着算法的进行,增量逐渐减少,直到只比较相邻元素的最后一趟排序为止。
- 提出者:该算法由D.L.Shell于1959年提出,并因此得名。
- 稳定性:希尔排序是非稳定排序算法,因为它在多次插入排序过程中可能会改变相同元素的相对顺序。
希尔排序的步骤如下:
- 先取一个步长,然后进行分组,然后进行插入排序
- 步长为步长/2,然后进行分组,然后进行插入排序
- 重复2,直到步长为1
希尔排序分析
- 时间复杂度 :O(N^2)
- 空间复杂度 :O(n)
- 稳定性 :不稳定
7 希尔排序示例
package cn.zxc.demo.leetcode_demo.base_algorithm.sort;
/**
* 希尔排序
* 1.先取一个步长,然后进行分组,然后进行插入排序
* 2.步长为步长/2,然后进行分组,然后进行插入排序
* 3.重复2,直到步长为1
*/
public class ShellSorting {
public static void main(String[] args) {
int[] nums = {12, 89, 34, 78, 65, 43, 21,
52, 98, 15, 37, 62, 85, 49, 28, 71, 94,
39, 58, 19, 87, 41, 68, 26, 91, 55, 32,
75, 17, 83, 46, 61, 97, 30, 79, 54, 25,
81, 48, 67, 14, 95, 36, 73, 29, 86, 42};
ShellSorting shellSorting = new ShellSorting();
int[] sort_nums = shellSorting.shellSort(nums, (nums.length / 2));
shellSorting.print(sort_nums);
sort_nums = shellSorting.shellSort2(nums, (nums.length / 2));
shellSorting.print(sort_nums);
}
/**
* 希尔排序:升序
* @param nums
* @param step
* @return
*/
public int[] shellSort(int[] nums, int step)
{
if (step == 0){
return nums;
}
// 以step为步长进行插入排序
for (int i = step; i < nums.length; i+=step) {
int temp = nums[i];
int j = i - step;
for (; j >=0; j-=step) {
if (nums[j] > temp){
nums[j+step] = nums[j];
}else{
break;
}
}
nums[j+step] = temp;
}
// 递归:每一次递归,步长变为原来的一半
return shellSort(nums, step/2);
}
/**
* 希尔排序:降序
* @param nums
* @param step
* @return
*/
public int[] shellSort2(int[] nums, int step)
{
if (step == 0){
return nums;
}
for (int i = step; i < nums.length; i+=step) {
int temp = nums[i];
int j = i - step;
for (; j >= 0; j-=step) {
if (nums[j] < temp){
nums[j+step] = nums[j];
}else{
break;
}
}
nums[j+step] = temp;
}
return shellSort2(nums, step/2);
}
public void print(int[] nums)
{
System.out.print("排序后:");
for (int i = 0; i < nums.length; i++) {
System.out.print(nums[i] + " ");
}
System.out.println();
}
}
8 归并排序简介
归并排序(Merge Sort)是一种基于分治思想的排序算法,它将待排序的数组分成两部分,分别对这两部分递归地进行排序,最后将两个有序子数组合并成一个有序数组。归并排序的基本思路是将待排序的数组分成两个部分,分别对这两部分进行排序,然后将排好序的两部分合并成一个有序数组。这个过程可以用递归来实现。
归并排序的步骤
- 分解:将待排序的数组不断分成两个子数组,直到每个子数组只有一个元素为止。这个过程可以使用递归来实现。
- 递归进行排序:对分解出的子数组进行归并排序。
- 合并:将相邻的两个已排序的子数组合并成一个有序数组,直到最后只剩下一个有序数组为止。合并的过程中,需要用到一个辅助数组来暂存合并后的有序数组。
- 创建一个临时数组来存放合并后的序列。
- 初始化两个指针,分别指向两个子序列的起始位置。
- 依次比较两个子序列中的元素,将较小的元素放入临时数组中,并将指向该元素的指针后移一位。
- 当其中一个子序列的指针移到末尾时,将另一个子序列中剩余的元素依次放入临时数组中。
- 将临时数组中的元素复制回原始序列的对应位置。
归并排序的分析
- 时间复杂度 :O(nlogn)
- 空间复杂度 :O(n),注意相比于插入排序,需要额外的O(n)的空间作为辅助
- 稳定性 :稳定
9 归并排序示例
import java.util.Arrays;
/**
* 归并排序
* 时间复杂度:O(nlogn)
* 空间复杂度:O(n)
* 实现思路:
* 1.将数组拆分成两个子数组,然后递归的拆分,直到拆分成单个元素
* 2.将两个子数组合并成一个有序数组
* 3.将两个有序数组合并成一个有序数组
*/
public class MergeSort {
public static void main(String[] args) {
int[] nums = {1, 3, 5, 2,8,6,10, 4, 6};
MergeSort mergeSort = new MergeSort();
int[] sort = mergeSort.mergeSort(nums, 0, nums.length - 1);
System.out.println(Arrays.toString(sort));
}
public int[] mergeSort(int[] nums, int left, int right)
{
if (right > left){
int mid = (left + right) / 2;
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
}
mergeNums(nums, left, right);
return nums;
}
private void mergeNums(int[] nums, int left, int right) {
// 为什么使用临时数据,不直接使用插入算法类型的数据插入的方式
// 因为归并排序,前后两个数组都是有序的,所以只需要比较两个数组的元素,然后插入到临时数组中
// 这样相比于数据插入的方式,需要的【数据比较】和【数据交换】的次数会减少很多
int[] temp = new int[right - left + 1];
int index = 0;
int l_s = left;
int r_s = (left + right) / 2 + 1;
int l_e = (left + right) / 2;
// 循环比较两个数组的元素,然后插入到临时数组中
while (l_s <= l_e && r_s <= right ){
if (nums[l_s] < nums[r_s]){
temp[index] = nums[l_s];
l_s++;
}else {
temp[index] = nums[r_s];
r_s++;
}
index++;
}
// 如果左边数组还有剩余,则直接插入到临时数组中
while (l_s <= l_e){
temp[index] = nums[l_s];
l_s++;
index++;
}
// 如果右边数组还有剩余,则直接插入到临时数组中
while (r_s <= right){
temp[index] = nums[r_s];
r_s++;
index++;
}
// 将临时数组中的数据插入到原数组中
for (int i = 0; i < temp.length; i++) {
nums[left++] = temp[i];
}
}
}