结合代码理解:快速排序、归并排序、堆排序算法的一些优缺点

本文详细介绍了快速排序、归并排序和堆排序三种常用的排序算法,讨论了它们的原理、常见问题、优化策略以及适用场景,包括性能优化、稳定性、内存使用和并行化等关键点。
摘要由CSDN通过智能技术生成

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:这里可以添加本文要记录的大概内容:

这几天突然想研究一下一些常用算法,今天就记录下最早接触到的排序算法吧。快速排序、归并排序和堆排序都是常用的排序算法,它们各自有独特的特点和适用场景,当然排序算法很多,比如冒泡啊,插入啊,选择排序啊等,不过他们的时间复杂度为O(n^2),通常不适用于大型数据集,就不提及了。
在这里插入图片描述
当对于数据量大,数据分布高度随机的场景,快速排序的平均效率要高于 堆排序和 归并排序;
对于Top- K问题,堆排序性能的下限(时间复杂度O(nlogn)要高于快排;
归并排序适合链表排序但不适合数组排序;归并在外部排序,比如磁盘文件的情况下比快排好,因为快排很依赖数据的随机存取,而归并是顺序存取,对磁盘这种外存比较友好
快排和堆排都是不稳定排序,归并排序是稳定排序。
实际场合中,快排对于数据量大,数据分布随机的平均效率高于堆排序,但如果数据大部分有序发现快排明显退化的时候会切换到堆排,快排递归到比较小的数据量的时候为了节约递归产生的空间,也会切换成插入排序;


提示:以下是本篇文章正文内容,下面案例可供参考

一、快速排序

快速排序是一种分治算法,由C. A. R. Hoare在1960年提出。它的基本思想是选择一个元素作为“基准”(pivot),然后重新排列数组,使得所有比基准小的元素都在基准的左边,所有比基准大的元素都在基准的右边。这个过程称为**分区(partitioning)。**之后,递归地对基准左边和右边的子数组进行同样的操作。

快速排序的步骤
选择基准:从数组中选择一个元素作为基准。(基准的选择对算法性能有很大影响。常见的选择方法包括首元素、末元素、中间元素或随机元素。)

分区:重新排列数组,所有小于基准的元素移动到基准的左边,所有大于基准的元素移动到基准的右边。

递归排序:对基准左边和右边的子数组进行快速排序。

快速排序的平均时间复杂度为O(n log n),但在最坏情况下(例如,数组已经排序或完全逆序)会退化到O(n^2)。为了提高性能,可以采用不同的基准选择策略,如随机选择基准或使用“三数取中”法。此外,对于小的子数组,可以使用插入排序来优化性能。

实例如下,在这个实例中,我们选择数组的最后一个元素作为基准:

public class QuickSort {

    // 快速排序的主要函数
    public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            // 分区索引
            int pi = partition(arr, low, high);

            // 分别对左右分区递归排序
            quickSort(arr, low, pi - 1);
            quickSort(arr, pi + 1, high);
        }
    }

    // 选择基准并进行分区操作的函数
    private static int partition(int[] arr, int low, int high) {
        // 选择末尾元素作为基准
        int pivot = arr[high];
        int i = (low - 1);

        for (int j = low; j < high; j++) {
            // 如果当前元素小于或等于基准
            if (arr[j] <= pivot) {
                i++;

                // 交换 arr[i] 和 arr[j]
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }

        // 交换 arr[i+1] 和 arr[high](或基准)
        int temp = arr[i + 1];
        arr[i + 1] = arr[high];
        arr[high] = temp;

        return i + 1;
    }

    // 测试快速排序的main函数
    public static void main(String[] args) {
        int[] arr = {10, 7, 8, 9, 1, 5};
        quickSort(arr, 0, arr.length - 1);

        System.out.println("Sorted array: ");
        for (int i : arr) {
            System.out.print(i + " ");
        }
    }
}

1.快速排序算法的常见问题

最坏情况性能:
如果每次选择的基准都是当前数组中的最小值或最大值(例如,数组已经排序或完全逆序),快速排序将退化到O(n^2)的时间复杂度。
避免方法:使用随机化版本的快速排序,即随机选择基准元素,可以大大降低最坏情况发生的概率。
递归导致的栈溢出:
快速排序的递归性质可能导致在处理非常大的数据集时栈溢出。
避免方法:对于非常大的数据集,可以使用非递归版本的快速排序,或者限制递归深度,结合使用其他排序算法(如插入排序)处理小数组。
非稳定排序:
快速排序不是稳定的排序算法,这意味着相等的元素在排序后可能会改变它们的原始顺序。
避免方法:如果稳定性是必需的,可以考虑使用稳定的排序算法,如归并排序或插入排序。
小数组性能差:
在小数组上,快速排序的性能可能不如某些简单的排序算法,如插入排序。
避免方法:可以设置一个阈值,当数组大小小于该阈值时,使用插入排序等简单算法代替快速排序。
数据分布不均匀:
如果数据分布非常不均匀,快速排序的性能可能会受到影响。
避免方法:可以通过随机化选择基准或使用“三数取中”法选择基准来减少这种情况的影响。
并行化难度:
快速排序的并行化可能比较复杂,因为需要仔细管理多线程之间的数据依赖和同步。
避免方法:可以考虑使用专门的并行排序算法或并行计算框架,如多线程快速排序或并行归并排序。
代码复杂度:
实现快速排序(特别是随机化版本)可能比实现一些简单的排序算法要复杂。
避免方法:对于不熟悉快速排序的开发者,可以使用标准库提供的排序函数,这些函数通常已经优化过,并且能够处理各种复杂情况。
内存访问模式:
快速排序可能导致较差的内存访问模式,因为元素的交换可能导致缓存未命中。
避免方法:可以通过优化数据结构和内存布局来提高缓存效率,或者使用其他具有更好内存访问模式的排序算法。

二、归并排序

归并排序(Merge Sort)是一种分治算法,由John von Neumann在1945年提出。它的核心思想是将数组分成两半,分别对这两半进行排序,然后将排序好的两半合并在一起。

归并排序的步骤
分解:如果数组只有一个元素或为空,则它已经排序好了。否则,将数组从中间分成两半。

递归排序:对两半分别递归地进行归并排序。

合并:将排序好的两半合并成一个有序数组。

组合:由于归并排序需要额外的内存空间来合并子数组,所以合并后的数组会被复制回原数组。

归并排序是一种稳定的排序算法,其时间复杂度在所有情况下都是O(n log n),但需要O(n)的额外空间来存储临时数组,这可能会影响其在内存受限的环境中的性能。

以下是Java中归并排序的一个简单实现,在这个实例中,我们首先将数组从中间分成两半,然后递归地对这两半进行归并排序。merge函数负责合并两个有序数组。运行main函数,将会输出排序后的数组。:

public class MergeSort {

    // 主要的归并排序函数
    public static void mergeSort(int[] arr, int left, int right) {
        if (left < right) {
            // 找到中间索引
            int middle = (left + right) / 2;

            // 分别对左右两半进行归并排序
            mergeSort(arr, left, middle);
            mergeSort(arr, middle + 1, right);

            // 合并排序好的左右两半
            merge(arr, left, middle, right);
        }
    }

    // 合并两个有序数组的函数
    private static void merge(int[] arr, int left, int middle, int right) {
        // 临时数组,用于存放合并后的有序数组
        int[] temp = new int[right - left + 1];
        int i = left;       // 左半边数组的索引
        int j = middle + 1; // 右半边数组的索引
        int k = 0;          // 临时数组的索引

        // 合并过程
        while (i <= middle && j <= right) {
            if (arr[i] <= arr[j]) {
                temp[k] = arr[i];
                i++;
            } else {
                temp[k] = arr[j];
                j++;
            }
            k++;
        }

        // 复制剩余的元素到临时数组
        while (i <= middle) {
            temp[k] = arr[i];
            i++;
            k++;
        }

        // 将临时数组复制回原数组
        for (int x = left; x <= right; x++) {
            arr[x] = temp[x - left];
        }
    }

    // 测试归并排序的main函数
    public static void main(String[] args) {
        int[] arr = {12, 11, 13, 5, 6, 7};
        mergeSort(arr, 0, arr.length - 1);

        System.out.println("Sorted array: ");
        for (int i : arr) {
            System.out.print(i + " ");
        }
    }
}

1.归并排序算法的常见问题

额外的内存使用:
归并排序需要与原始数组相同大小的额外内存空间来存放合并后的有序数组。
避免方法:如果内存使用是一个限制因素,可以考虑使用原地排序算法,如快速排序或堆排序。
栈空间限制:
归并排序的递归实现可能会因为数据集过大而导致栈溢出。
避免方法:可以使用非递归版本的归并排序来避免这个问题。
数据局部性:
归并排序在合并过程中可能会破坏数据的局部性,影响缓存的性能。
避免方法:对于需要高缓存性能的应用,可以考虑使用具有更好数据局部性的排序算法,如插入排序或归并插入排序。
不适合小数据集:
归并排序在小数据集上的性能可能不如一些简单的排序算法,如插入排序或选择排序。
避免方法:对于小数据集,可以使用更简单的排序算法,或者设置一个阈值,在数据集大小小于该阈值时使用简单排序算法。
递归实现的复杂性:
归并排序的递归实现可能比非递归实现更难理解和实现。
避免方法:如果递归逻辑是一个问题,可以使用非递归的实现,或者使用其他非递归排序算法。
并行化难度:
虽然归并排序可以并行化,但并行合并步骤的实现可能比较复杂。
避免方法:如果需要并行化,可以考虑使用专门的并行排序算法或并行计算框架。
稳定性的维护:
在合并过程中,保持稳定性需要额外的注意,以确保相等元素的相对顺序不变。
避免方法:在合并时,可以设计算法以确保当两个相等的元素相遇时,来自原始数组中较左侧的元素被保留。
代码复杂度:
归并排序的实现相对复杂,特别是对于不熟悉递归或分治策略的开发者。
避免方法:可以使用标准库提供的排序函数,这些函数通常已经优化过,并且能够处理各种复杂情况。


三、堆排序

堆排序(Heap Sort)是一种基于比较的排序算法,它利用了二叉堆的数据结构来实现排序。堆排序分为两个主要部分:将数据构建成堆,以及将堆顶元素与末尾元素交换并重新调整堆结构。

堆排序的步骤
构建最大堆:将给定的无序数列构建成一个大顶堆。

交换堆顶元素与最后一个元素:将堆顶元素(最大元素)与末尾元素交换。

重新调整堆结构:重新调整堆结构,使其满足大顶堆的性质。

重复步骤2和3:重复交换堆顶元素与末尾元素,然后重新调整堆,直到堆中只剩下一个元素。

堆排序的时间复杂度在所有情况下都是O(n log n),并且它是原地排序算法,不需要额外的存储空间。然而,由于其递归性质,堆排序在处理大规模数据集时可能会受到堆栈空间限制的影响。

以下是Java中堆排序的一个简单实现,在这个实例中,我们首先构建了一个最大堆,然后交换堆顶元素(最大元素)与末尾元素,接着重新调整堆结构,最后递归地进行上述操作直到堆中只剩下一个元素。运行main函数,将会输出排序后的数组。:

public class HeapSort {

    // 构建最大堆
    public static void buildMaxHeap(int[] arr, int n, int i) {
        int largest = i; // 假设当前节点为最大值节点
        int left = 2 * i + 1;  // 左子节点
        int right = 2 * i + 2; // 右子节点

        // 如果左子节点存在且大于当前节点
        if (left < n && arr[left] > arr[largest]) {
            largest = left;
        }

        // 如果右子节点存在且大于当前节点
        if (right < n && arr[right] > arr[largest]) {
            largest = right;
        }

        // 如果最大节点不是当前节点,交换它们并继续调整
        if (largest != i) {
            swap(arr, i, largest);
            buildMaxHeap(arr, n, largest);
        }
    }

    // 交换两个元素
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    // 堆排序的主要函数
    public static void heapSort(int[] arr) {
        int n = arr.length;

        // 构建最大堆
        for (int i = n / 2 - 1; i >= 0; i--) {
            buildMaxHeap(arr, n, i);
        }

        // 交换堆顶元素与末尾元素并重新调整堆结构
        for (int i = n - 1; i >= 0; i--) {
            swap(arr, 0, i);
            buildMaxHeap(arr, i, 0);
        }
    }

    // 测试堆排序的main函数
    public static void main(String[] args) {
        int[] arr = {12, 11, 13, 5, 6, 7};
        heapSort(arr);

        System.out.println("Sorted array: ");
        for (int i : arr) {
            System.out.print(i + " ");
        }
    }
}

1.堆排序算法的常见问题

堆排序算法在实际应用中可能会遇到一些问题,以下是一些常见的问题以及相应的避免方法:

最坏情况性能:
堆排序的最坏情况时间复杂度是O(n log n),但这是在特定输入下才会发生的,例如输入数组已经是完全逆序的。在大多数实际应用中,堆排序的性能通常接近最优。
避免方法:由于堆排序的最坏情况并不常见,通常不需要特别避免。如果确实关心最坏情况性能,可以考虑使用其他算法,如快速排序或归并排序。

非稳定排序:
堆排序不是稳定的排序算法,这意味着相等的元素在排序后可能会改变它们的原始顺序。
避免方法:如果稳定性是必需的,可以考虑使用稳定的排序算法,如归并排序或插入排序。

原地排序的空间复杂度:
堆排序是原地排序算法,它不需要额外的存储空间,但需要空间来维护堆结构,这可能会对内存使用产生影响。
避免方法:如果内存使用是一个问题,可以考虑使用其他原地排序算法,如快速排序。

构建初始堆的时间开销:
在堆排序中,构建初始堆的时间复杂度是O(n),这可能会增加算法的总体时间开销。
避免方法:对于小规模数据集,构建初始堆的开销可能不是问题。对于大规模数据集,可以考虑使用其他排序算法,或者优化堆的构建过程。

递归实现的栈空间限制
堆排序的递归实现可能会因为数据集过大而导致栈溢出。
避免方法:可以使用非递归版本的堆排序来避免这个问题,或者使用尾递归优化的版本。

数据局部性:
堆排序在操作过程中可能会破坏数据的局部性,这可能会影响缓存的性能。
避免方法:对于需要高缓存性能的应用,可以考虑使用其他具有更好数据局部性的排序算法,如插入排序或归并排序。

不适合小数据集:
由于构建堆和堆调整的开销,堆排序在小数据集上的性能可能不如一些简单的排序算法,如插入排序或选择排序。
避免方法:对于小数据集,可以使用更简单的排序算法,或者设置一个阈值,在数据集大小小于该阈值时使用简单排序算法。

代码复杂度:
堆排序的实现相对复杂,特别是对于不熟悉堆数据结构的开发者。
避免方法:可以使用标准库提供的排序函数,这些函数通常已经优化过,并且能够处理各种复杂情况。

  • 40
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值