排序算法

本文详细介绍十种经典排序算法,包括冒泡排序、选择排序、堆排序等,涵盖各种算法的时间复杂度、空间复杂度及其实现原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

排序算法

均按照升序演示

排序算法的稳定性:如果相等的两个元素,在排序前后的相对位置保持不变,我们就称为是稳定的排序算法,否则是不稳定的。

原地算法 In-place:不依赖额外的资源或依赖少数的额外资源,仅依靠输出来覆盖输入,空间复杂度为O(1)的都可以认为是原地算法

1、冒泡排序(Bubble Sort)

时间复杂度:O(n^2) 空间复杂度O(1) 可以是稳定的排序算法

思想

  1. 从头开始比较相邻的一对元素,如果左边的比右边的大,就交换两数位置
  2. 忽略到最大的元素,从剩下的元素中进行一边冒泡排序,直到使数组变为有序

代码:

// 冒泡
public void sort(){
    for (int end = array.length - 1; end > 0; end--){
        for (int begin = 1; begin <= end; begin++){
            if (cmpIndex(begin, begin-1) < 0){
                swap(begin, begin-1);
            }
        }
    }
}

改进优化1

引入标记flag,在内层循环中,如果发现没有进入if子句,就说明该数组已经排好了,直接break即可

public void sort() {
    for (int end = array.length - 1; end > 0; end--) {
        boolean flag = true;
        for (int begin = 1; begin <= end; begin++){
            if (cmpIndex(begin, begin - 1) < 0){
                flag = false;
                swap(begin, begin - 1);
            }
        }
        if (flag) break;
    }
}

改进优化2

如果序列尾部已经局部有序,可以记录最后一次交换的位置,减少比较次数

@Override
public void sort() {
    for (int end = array.length - 1; end > 0; end--){
        int sortIndex = 1;
        for (int begin = 1; begin <= end; begin++){
            if (cmpIndex(begin, begin - 1) < 0){
                swap(begin, begin - 1);
                sortIndex = begin;
            }
        }
        end = sortIndex;
    }
}

2、选择排序(Select Sort)

时间复杂度:O(n^2) 空间复杂度O(1) 不稳定的排序算法

思想:从序列中找出最大的元素,然后和最末尾的元素交换位置。或者是从序列中找出最小的元素和第一个交换位置

代码

public static void selectSort(int[] nums){
  for (int i = 0; i < nums.length - 1; i++){
    int target = i;
    int min = nums[i];
    for (int j = i; j < nums.length; j++){
      if (nums[j] < min) {    // 遍历后面的数找到最小值
        min = nums[j];
        target = j;
      }
    }
    // 将nums[i] 和 nums[j] 交换一下即可
    int temp = nums[i];
    nums[i] = nums[target];
    nums[target] = temp;
  }
}

3、堆排序(Heap Sort)

可以认为是对选择排序的一种优化,因为它在内层循环中挑选最大值的时候时间复杂度较低

  1. 必须是一棵完全二叉树
  2. 对于堆里面的值,父节点必须大于子节点

思想

  1. 对序列进行原地建堆(heapify)O(n)
  2. 重复以下操作,直到堆的元素数量为1 O(n)
    • 交换堆顶元素与尾元素
    • 堆的元素数量减一
    • 对0号位置进行一次shitdown操作 O(log(n))

4、快速排序(Quick Sort)

逐渐地将每一个元素转化为轴点元素

思想:通过一趟排序将要排序的数据分割为两部分,其中一部分的所有数据都要比另外一部分的所有数据要小,然后再按照此方法对两部分数据分别进行快速排序,使整个数据变为有序数据

  1. 选定Pivot中心轴
  2. 将大于Pivot的数字放在Pivot右边
  3. 将小于Pivot的数字放在Pivot左边
  4. 分别对左右子序列重复前三步操作

实现方式: 左右指针

@Override
public void sort(){
    quickSort(array, 0, array.length - 1);
}

public void quickSort(Integer[] array, int left, int right){
    int pivot = array[left];       // array[0]作为中心轴
    int begin = left;
    int end = right;

    while (left < right){
        while (left < right){
            if (cmpElements(pivot, array[right]) < 0){   // 右边元素大于pivot
                right--;
            }else {                     // 右边元素小于等于pivot
                array[left] = array[right];
                left++;
                break;
            }
        }

        while (left < right){
            if (cmpElements(pivot, array[left]) > 0){    // 左边元素小于轴点
                left++;
            } else {                    // 左边元素大于等于轴点
                array[right] = array[left];
                right--;
                break;
            }
        }
    }

    array[left] = pivot;

    if (cmpElements(left, begin+1) > 0){
        quickSort(array, begin, left - 1);
    }

    if (cmpElements(end, left + 1) > 0){
        quickSort(array, left + 1, end);
    }
}

除次之外,用快慢指针的方式也可以实现,关键在于一遍遍历找到左右两部分数据这个操作

5、插入排序(Insertion Sort)

类似于扑克牌排序

思想

  1. 在执行的过程中,插入排序会将序列分为两部分头部和尾部(头部是已经拍好序的,尾部是待排序的)
  2. 从头开始扫描每一个元素,每当扫描到一个元素,就将它插到头部合适的位置,使头部数据依然有序

public void sort() {
    // begin 代表你起的牌的索引(这里直接从第二张牌开始), 要插到前面合适的位置
    for (int begin = 1; begin < array.length; begin++) {
        int cur = begin;
        while(cmpIndex(cur, cur - 1) < 0){
            swap(cur, cur - 1);
            if (--cur <= 0) break;
        }
    }
}

逆序对

  • 数组<2, 3, 8, 6, 1> 的逆序对是<2,1> ❤️,1> <8,6> <8,1> <6,1>
  • 插入排序的时间复杂度与逆序对的数量成正比

插入排序的优化算法:(将交换转为挪动)

  1. 先将待插入的元素备份
  2. 头部有序数据中比待插元素大的,朝尾部方向挪动一个位置
  3. 将待插元素备份放到空出的位置
for (int begin = 1; begin < array.length; begin++) {
    int cur = begin;
    int val = array[begin];
    while (cmpIndex(cur, cur - 1) < 0){
        array[cur] = array[cur-1];
        if (--cur <= 0) break;
    }
    array[cur] = val;
}

插入排序的优化算法:(二分搜索):

  • 在插入元素v的过程中,可以先二分搜索出合适的插入位置,然后在进行插入,优化了比较次数,但挪动没有办法优化

    @Override
    public void sort() {
      for (int begin = 1; begin < array.length; begin++) {
          // 要插入的值
          int val = array[begin];
          // 要插入的位置
          int position = search(begin);
          int cur = begin;
          // 移动元素, 索引大的先移动
          for (;cur > position;cur--){
              array[cur] = array[cur-1];
          }
          array[position] = val;
      }
    }
    
    
    //供插入排序使用的二分搜索, 返回要插入元素的待插入位置索引
    // 返回的位置是从左到右第一个大于target的元素的位置索引
    
    private int search(int index){
        int begin = 0;
        int end = index;
        while (begin < end){
            int mid = (begin + end) >> 1;
            if (cmpIndex(index, mid) < 0){
                end = mid;
            }else {
                begin = mid + 1;
            }
        }
        return begin;
    }
    
    
    

6、希尔排序(Shell Sort)

相当于改进的插入排序。

思想: 是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。 Shell算法的性能与所选取的分组长度序列有很大关系。只对特定的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数

@Override
public void sort() {
  List<Integer> integers = shellStepSequece();
  // 对每个步长进行选择排序
  for (Integer integer : integers) {
      sort(integer);
  }
}

public void sort(int step){
  for (int col = 0; col < step; col++) {
      // 对每列进行选择排序

      // 插入排序
      for (int begin = col + step; begin < array.length; begin += step){
          int cur = begin;
          while (cur > col && cmpIndex(cur, cur - step) < 0){
              swap(cur, cur - step);
              cur -= step;
          }
      }

  }
}

// 获取步长序列, 这里的序列是2以及2的倍数,是shell推荐的
private List<Integer> shellStepSequece(){
  List<Integer> stepSequence = new ArrayList<>();
  int step = array.length;
  while ((step /= 2) > 0){
      stepSequence.add(step);
  }
  return stepSequence;
}

二分搜索(Binary Search)

思路:

在一个排好序列的数组中,begin为0,end为length

  1. 假设我们在 [begin, end) 范围中搜索某个元素v,mid = (begin + end) / 2
  2. 如果等于m,则查找完成
  3. 如果小于v,则将end置为mid,在左半部分重复上面的操作
  4. 如果大于m,则将begin置为mid+1,在右半部分重复上面的操作
  5. 如果begin = end了,则查找失败

递归实现:

// 如果查找不到返回-1
public static int indexOf(int[] array, int begin, int end, int target){
    if (array.length == 0 && array == null) return -1;

    while (begin < end){
        int mid = (begin + end) >> 1;
        if (target == array[mid]) {
            return mid;
        }else if (target < array[mid] ){
            return indexOf(array, begin, mid, target);
        }else if (target > array[mid]){
            return indexOf(array, mid + 1, end, target);
        }
    }
    return -1;
}


public static int indexOf(int[] array, int target){
    return indexOf(array, 0, array.length, target);
}

非递归实现:

public  int indexOf2(int[] array, int target){
    if (array.length == 0 && array == null) return -1;
    int begin = 0;
    int end = array.length;
    while (begin < end){
        int mid = (begin + end) >> 1;
        if (target < array[mid]){
            end = mid;
        }else if (target > array[mid]){
            begin = mid + 1;
        }else {
            return mid;
        }
    }
    return -1;
}

7、归并排序(Merge Sort)

思想:

  1. 不断地将当前序列平均分割成两个子序列,分割到不能在分割为止(序列中只有一个元素)(divide)
  2. 不断将两个子序列按照大小顺序合并成一个有序序列,直到最终只剩下一个有序序列(merge)
public void sort() {
    sort(0, array.length);
}

/**
     * 对 [begin, end) 范围内的数据进行归并排序
     * @param begin
     * @param end
     */
private void sort(int begin, int end){
    if (end - begin < 2) return;
    int mid = (begin + end) >> 1;

    sort(begin, mid);
    sort(mid, end);
    merge(begin, mid, end);
}

/**
     * 将 [begin, mid) [mid, end) 范围的有序序列合并成一个有序序列
     */
private void merge(int begin, int mid, int end){
    int li = 0, le = mid - begin;   // leftArray的索左右
    int ri = mid, re = end;         // array 右半部分的左右
    int ai = begin;                 // array 左半部分的指针

    // 备份左边数组
    int[] leftArray = new int[mid - begin];
    for (int i = 0; i < leftArray.length; i++) {
        leftArray[i] = array[begin + i];
    }

    /*
         * 这里和算法题目中不一样的是,就算先跳出循环,array中没有被赋值的地方也有原本的值
         * 并且这些值正好满足排序,所以不用担心先break掉的情况
         */
    while (li < le){
        if (ri < re && leftArray[li] > array[ri]){
            array[ai++] = array[ri++];
        }else {
            array[ai++] = leftArray[li++];
        }
    }
}

8、计数排序(Counting Sort)

适合对一定范围内的元素进行排序

思路: 创建一个基于要排序数组最大值的数组,其索引代表原数组的值,其值代表该索引所代表的值在原数组出现的次数,然后便利该数组将索引放回原数组中即可

实现

public void sort() {
    // 先找到最大值
    int max = array[0];
    for (int i = 0; i < array.length; i++) {
        if (array[i] > max) max = array[i];
    }
    // 创建最大值空间的数组,并将array填入
    int[] arrayRef = new int[max+1];
    for (int i = 0; i < array.length; i++) {
        arrayRef[array[i]]++;
    }
    // 将数组遍历填入原数组中
    int j = 0;
    for (int i = 0; i < arrayRef.length; i++) {
        while (arrayRef[i]-- > 0){
            array[j++] = i;
        }
    }
}

上面的实现是最简单的实现,它只能排正整数,不稳定,并且及其浪费空间

改进方法:

public void sort() {
    // 先找到最大值,最小值
    int max = array[0];
    int min = array[0];
    for (int i = 1; i < array.length; i++) {
        if (array[i] > max) max = array[i];
        if (array[i] < min) min = array[i];
    }

    // 创建最大值空间的数组,并将array填入,累加的次数

    int[] arrayRef = new int[max - min + 1];
    for (int i = 0; i < array.length; i++) {
        arrayRef[array[i] - min]++;
    }
    // 累加次数
    for (int i = 1; i < arrayRef.length; i++) {
        arrayRef[i] += arrayRef[i-1];
    }


    // 从后往前遍历,将之放到有序数组上,从后往前可以保证稳定性
    Integer[] arrayCopy = array.clone();
    for (int i = array.length-1; i >= 0 ; i--) {
        array[--arrayRef[arrayCopy[i] - min]] = arrayCopy[i];
    }
}

9、基数排序(Radix Sort)

适合整数排序(尤其是正整数)

思路: 依次对个位数,十位数,百位数…进行计数排序,排完即有序

@Override
public void sort() {
    // 找最大值,确定位数,即要计数排序的次数
    int max = array[0];
    for (int i = 1; i < array.length; i++) {
        if (array[i] > max) max = array[i];
    }

    /**
         * max = 593
         * 个位数:593 / 1 % 10 = 3
         * 十位数:593 / 10 % 10 = 9
         * 百位数:593 / 100 % 10 = 5
         * 千位数:593 / 1000 % 10 = 0
         */
    for (int divider = 1; divider <= max; divider *= 10) {
        countingSort(divider);
    }

}

// 适用于基数排序的计数排序
private void countingSort(int divider){


    int[] arrayRef = new int[10];
    for (int i = 0; i < array.length; i++) {
        arrayRef[array[i] / divider % 10]++;
    }
    // 累加次数
    for (int i = 1; i < arrayRef.length; i++) {
        arrayRef[i] += arrayRef[i-1];
    }

    // 从后往前遍历,将之放到有序数组上
    Integer[] arrayCopy = array.clone();
    for (int i = array.length-1; i >= 0 ; i--) {
        array[--arrayRef[arrayCopy[i] / divider % 10]] = arrayCopy[i];
    }
}

10、桶排序(Bucket Sort)

思路:

  1. 创建一定数量的桶(数组,链表)
  2. 按照一定的规则(不同类型的数据规则不同,整数\小数),将桶中的元素均匀分配到对应的桶
  3. 分别对每个桶单独排序
  4. 将所有非空桶的元素合并成有序序列
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值