【学习记录】十大排序算法--Java实现

37 阅读9分钟

1 插入排序

1.1 基本思想

每次将一个待排序的元素插入按其关键字大小插入到前面已排序好的子序列中,直到全部记录插入完成。首次将第一个元素看作是已排好序的子序列,从第二个元素开始遍历。

1.2 代码实现

直接插入排序:顺序查找找到插入的位置,再移动元素,适用于顺序表、链表

public static void insertSort(int[] arr) {
    int len = arr.length;
    for (int i = 1; i < len; i++) {
        if (arr[i] < arr[i - 1]) {
            int temp = arr[i];
            int j;
            for (j = i - 1; j >= 0 && arr[j] > temp; j--) {
                arr[j + 1] = arr[j];
            }
            arr[j + 1] = temp;
        }
    }
}

折半插入排序:折半查找找到插入的位置,再移动元素,这种方法减少了元素对比的个数,移动的个数没有改变,仅适用于顺序表

public static void binaryInsertSort(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        int left = 0;
        int right = i - 1;
        int temp = arr[i];
        while (left <= right) {
            int mid = (left + right) / 2;
            if (temp < arr[mid]) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        for (int j = i - 1; j >= left; j--) {
            arr[j + 1] = arr[j];
        }
        arr[left] = temp;
    }
}

1.3 性能

空间复杂度:O(1)

时间复杂度:

  • 最好:原本有序O(n)
  • 最坏:原本逆序O(n^2)
  • 平均:O(n^2)

稳定性:相同元素的相对位置没有改变,稳定

2 希尔排序

2.1 基本思想

先将待排序表分割成若干形如L[i,i+d,i+2d,...i+kd]的“特殊”子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直接d=1(之前的过程已经将待排序表基本排成有序,最后一步对整个表进行直接插入排序)为止。

2.2 代码实现

private static void shellSort(int[] arr) {
    int len = arr.length;
    int d, i, j, temp;
    for (d = len / 2; d > 0; d /= 2 ) {  // 步长变化
        for (i = d ; i < len; i++) {
            if (arr[i] < arr[i - d]) { // 提前判断,避免不必要的赋值
                temp = arr[i];
                for (j = i - d; j >= 0 && temp < arr[j]; j -= d) {
                    arr[j + d] = arr[j]; // 记录后移,查找插入的位置
                }
                arr[j + d] = temp; // 插入目标位置
            }
        }
    }
}

2.3 性能

空间复杂度:O(1)

时间复杂度:目前无法用数学手段证明确切的时间复杂度

  • 最坏:原本逆序O(n^2)
  • 当n在某个范围内时:O(n^1.3)

稳定性:相同元素的相对位置发生改变,不稳定

3 冒泡排序

3.1 基本思想

从前往后(或从后往前)两两比较相邻元素的值,若为逆序,则交换它们,直到序列比较完,称这样的过程为一趟冒泡排序(最多n-1趟),每一趟排序可以使一个元素移动到最终位置,已经确定的元素后续不需要比较。

3.2 代码实现

private static void bubbleSort(int[] arr) {
    int len = arr.length;
    for (int i = 0; i < len - 1; i++) {
        boolean flag = false; // 标记本趟排序是否进行交换
        for (int j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) { // 当前元素比下一个元素大,交换
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                flag = true; // 发生交换,标记为true
            }
        }
        if (!flag) {
            return; // 本趟排序没有发生交换,说明已经有序,可以提前结束排序
        }
    }
}

3.3 性能

空间复杂度:O(1)

时间复杂度:

  • 最好:原本有序O(n)
  • 最坏:原本逆序O(n^2)
  • 平均:O(n^2)

稳定性:相同元素的相对位置没有改变,稳定

4 快速排序

4.1 基本思想

在待排序表中任取一个元素pivot作为枢轴(通常取首元素),通过一趟排序将待排序表划分成为独立的两部分,一部分元素小于pivot,另一部分大于pivot,最终将pivot放到最终位置,这个过程称为一次划分。然后分别递归地对两个子表重复上述过程,直到每部分只有一个元素或为空为止,即所有元素放在了其最终位置上。

4.2 代码实现

private static void quickSort(int[] arr, int low, int high) {
    if (low < high) { // 递归结束条件
        int pivot = partition(arr, low, high); // 划分
        quickSort(arr, low, pivot - 1); // 划分左子表
        quickSort(arr, pivot + 1, high); // 划分右子表
    }
}

private static int partition(int[] arr, int low, int high) {
    int pivot = arr[low]; // 选择第一个元素作为枢轴元素
    while (low < high) {
        while (low < high && arr[high] >= pivot) high--;
        arr[low] = arr[high]; // 比枢轴元素小的移动到左边
        while (low < high && arr[low] <= pivot) low++;
        arr[high] = arr[low]; // 比枢轴元素大的移动到右边
    }
    arr[low] = pivot; // 枢轴元素放到最终位置
    return low; // 返回枢轴元素的最终位置
}

4.3 性能

算法表现主要取决于递归深度,若每次划分越均匀,则递归深度越低,划分越不均匀,递归深度越深。

空间复杂度:

  • 最好:O(logn)
  • 最坏:O(n)

时间复杂度:

  • 最好:每次划分很平均O(nlogn)
  • 最坏:原本逆序O(n^2)
  • 平均:O(nlogn)

稳定性:相同元素的相对位置发生改变,不稳定

5 简单选择排序

5.1 基本思想

每一趟在待排序元素中选取最小的元素加入有序子序列,必须进行n-1趟的处理

5.2 代码实现

private static void selectSort(int[] arr) {
    int len = arr.length;
    for (int i = 0; i < len - 1; i++) {
        int minIndex = i; // 记录最小值位置
        for (int j = i + 1; j < len; j++) { // 选择最小元素
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        if (minIndex != i) { // 交换最小值和当前值
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
}

5.3 性能

空间复杂度:O(1)

时间复杂度:O(n^2)

稳定性:相同元素的相对位置发生改变,不稳定

6 堆排序

6.1 基本思想(以大顶堆为例)

建堆:从最后一个非叶节点(从右向左,由下而上)依次“下坠”调整,小元素与关键字更大的孩子交换。

排序:将堆顶元素与堆底元素交换后需要进行“下坠”调整,恢复到大顶堆的特性。上述过程重复n-1趟。

6.2 代码实现

private static void heapSort(int[] arr) {
    int len = arr.length;
    // 建堆
    // 初始化大顶堆(从最后一个非叶节点开始,从右到左,由下到上)
    for (int i = len / 2 - 1; i >= 0; i--) {
        adjustHeap(arr, i, len);
    }

    // 排序
    // 将顶节点和最后一个节点互换位置,再将剩下的堆进行调整
    for (int i = arr.length - 1; i > 0; i--) {
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;
        adjustHeap(arr, 0, i);
    }
}

private static void adjustHeap(int[] arr, int k, int len) {
    int temp = arr[k]; // 临时变量保存当前节点
    for (int i = 2 * k + 1; i < len; i = 2 * i + 1) { // i是当前非叶节点的左子节点下标
        if (i + 1 < len && arr[i] < arr[i + 1]) { // 如果有右子节点,并且右子节点大于左子节点
            i++; // i指向右子节点
        }
        if (arr[i] > temp) { // 如果子节点大于当前节点,将子节点值赋给当前节点
            arr[k] = arr[i];
            k = i; // k指向交换节点的下标,以便继续向下筛选
        } else {
            break; // 否则,说明当前节点的子节点已经比当前节点大,不需要再比较了
        }
    }
    arr[k] = temp; // 将临时变量中的值赋给当前节点
}

6.3 性能

空间复杂度:O(1)

时间复杂度:建堆O(n),排序O(nlogn);总的时间复杂度为O(nlogn)

稳定性:相同元素的相对位置发生改变,不稳定

7 归并排序

7.1 基本思想

递归拆分:先把待排序数组分为左右两个子序列,再分别将左右两个子序列拆分为四个子子序列,以此类推直到最小的子序列元素的个数为两个或者一个为止。 逐步合并:将最底层的最左边的一个子序列排序,然后将从左到右第二个子序列进行排序,再将这两个排好序的子序列合并并排序,然后将最底层从左到右第三个子序列进行排序..... 合并完成之后记忆完成了对数组的排序操作(一定要注意是从下到上层级合并,可以理解为递归的层级返回)

7.2 代码实现

private static void mergeSort(int[] arr, int left, int right) {
    if (left < right) {
        int mid = (left + right) / 2; // 中间位置
        mergeSort(arr, left, mid); // 递归拆分左边
        mergeSort(arr, mid + 1, right); // 递归拆分右边
        merge(arr, left, mid, right); // 合并左右两边
    }
}

private static void merge(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 (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[left + m] = temp[m];
    }
}

7.3 性能

空间复杂度:O(n)

时间复杂度:O(nlogn)

稳定性:相同元素的相对位置不改变,稳定

8 基数排序

8.1 基本思想

将整个关键字拆分为d位(或d组),按照各个关键字位权重递增次序(如:个、十、百)做d趟“分配”和“收集”。

分配:顺序扫描各个元素,根据当前处理的关键字位,将元素插入相应队列,一趟分配耗时O(n)。

收集:把各个队列中的节点依次出队并链接,一趟收集耗时O(r),r是队列个数。

8.2 性能

空间复杂度:O(r)

时间复杂度:O(d(n+r))

稳定性:相同元素的相对位置不改变,稳定

9 计数排序

9.1 基本思想

统计频次:遍历待排序数组,找出最大值max和最小值min,创建一个计数数组count,长度为max-min+1,然后统计每个整数出现的次数,存入count对应的索引。

计算前缀和(位置信息):对count数组进行前缀和计算,即count[i]更新为count[1]+count[2]+...+count[i]。此时count[k]表示值小于等于k的元素个数。

反向填充排序数组:从后往前遍历原数组(为了保证稳定性),根据当前元素值查找count中对应的位置,将该元素放入排序数组的指定位置,然后将count中对应的值减1。

9.2 性能

空间复杂度:O(n+k),其中O(n)由输出数组导致,O(k)由辅助数组导致

时间复杂度:O(n+k)

稳定性:相同元素的相对位置不改变,稳定

10 外部排序

10.1 基本思想

外部排序(External Sorting)是用于处理数据量太大,无法全部加载到内存中的排序算法。其基本思想是分阶段处理:先部分排序,再归并。

优化:为了减少读写外存的时间,使用多路归并,在内存里面分配多个输入缓存区。