深入探索Java中的多种排序算法及其应用场景

引言

在Java编程中,排序算法作为基础且关键的数据结构技术,对于提升程序性能和优化资源利用至关重要。本文将详细介绍Java中常用的几种排序算法,探讨其工作原理、性能分析以及适用场景。

一、冒泡排序(Bubble Sort)

冒泡排序(Bubble Sort)是一种简单的排序算法,通过重复遍历列表并对相邻元素进行比较和交换来排序列表中的元素。它的名称来源于排序过程中元素像气泡一样逐渐“浮”到列表的顶部(或底部)。


Java冒泡排序示例代码:

public class BubbleSortExample {
    public static void main(String[] args) {
        int[] arr = {5, 3, 8, 1, 2, 9}; // 待排序的数组
        bubbleSort(arr); // 调用冒泡排序方法
        System.out.println("Sorted array:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }

    public static void bubbleSort(int[] arr) {
        int n = arr.length;
        boolean swapped; // 标记是否发生过交换

        // 外层循环决定需要多少轮冒泡操作
        for (int i = 0; i < n - 1; i++) {
            swapped = false; // 初始化标记为false

            // 内层循环负责每一轮的冒泡操作
            for (int j = 0; j < n - 1 - i; j++) {
                // 如果当前元素比下一个元素大,则交换它们的位置
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    swapped = true; // 若发生交换,标记为true
                }
            }

            // 如果一轮冒泡没有发生任何交换,则数组已经是有序的,可以提前结束排序
            if (!swapped) break;
        }
    }
}

详细讲解:

  1. 外层循环:初始化为 for (int i = 0; i < n - 1; i++),这里的 n 是数组长度。每一轮循环都将确定最大的一个元素“冒泡”到数组的末尾。
  2. 内层循环:对于每一轮外层循环,内层循环 for (int j = 0; j < n - 1 - i; j++) 会遍历数组的一段子区间。它将比较相邻的元素,如果前面的元素大于后面的元素,则交换它们的位置。
  3. 交换元素:通过临时变量 temp 存储较大的值,然后将较小值赋给前一个索引,将 temp 中的较大值赋给后一个索引。
  4. 无交换标志:引入布尔变量 swapped,用于跟踪是否在本轮内发生了交换。如果没有发生交换,说明数组剩余部分已经有序,因此可以直接跳出外层循环,避免不必要的比较。

通过这样的迭代过程,冒泡排序逐步将最大的元素推向数组的末端,直到整个数组变得有序。冒泡排序的时间复杂度在最坏情况下和平均情况下都是 O(n^2),在最佳情况下(已排序数组)为 O(n)。

二、选择排序(Selection Sort)

Java选择排序(Selection Sort)示例代码:

public class SelectionSortExample {
    public static void main(String[] args) {
        int[] arr = {9, 5, 2, 7, 1, 6, 3, 8, 4}; // 待排序的数组
        selectionSort(arr); // 调用选择排序方法
        System.out.println("Sorted array:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }

    public static void selectionSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            // 找到当前未排序部分的最小元素的索引
            int minIndex = i;
            for (int j = i + 1; j < n; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }

            // 将找到的最小元素与第一个未排序位置的元素交换
            if (minIndex != i) {
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            }
        }
    }
}

详细讲解:

  1. 总体流程:选择排序的核心思想是每一次从未排序的部分中找到最小(或最大)的元素,然后将其放到已排序部分的末尾。重复这个过程,直到整个数组都被排序。
  2. 外层循环:初始化为 for (int i = 0; i < n - 1; i++),这里的 n 是数组长度。每一次循环,都会处理一个未排序的元素,并将其置于正确的位置。
  3. 寻找最小元素:在内层循环 for (int j = i + 1; j < n; j++) 中,我们遍历未排序部分(从 i+1 开始),查找剩余部分中的最小元素。一旦找到更小的元素,就更新 minIndex 变量,保存最小元素的索引。
  4. 交换元素:在内层循环结束后,我们将找到的最小元素(位于 arr[minIndex])与当前未排序的第一个元素(位于 arr[i])进行交换。如果 minIndex 不等于 i,这意味着找到了一个更小的元素,需要进行交换。
  5. 重复过程:随着外层循环的推进,每一轮都会将当前最小元素移到正确的位置。经过 n-1 轮循环后,整个数组就被排序完毕。

选择排序的时间复杂度在最好、最坏和平均情况下都是 O(n^2),因为它总是需要对整个未排序部分进行比较。这种排序算法的特点是交换次数较少,但比较次数较多。

三、插入排序(Insertion Sort)

Java插入排序(Insertion Sort)示例代码:

public class InsertionSortExample {
    public static void main(String[] args) {
        int[] arr = {9, 5, 2, 7, 1, 6, 3, 8, 4}; // 待排序的数组
        insertionSort(arr); // 调用插入排序方法
        System.out.println("Sorted array:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }

    public static void insertionSort(int[] arr) {
        int n = arr.length;
        for (int i = 1; i < n; i++) {
            int key = arr[i]; // 当前待插入的元素
            int j = i - 1; // 查找插入位置的起始索引

            // 将key元素逐步向前移,直到找到正确的位置
            while (j >= 0 && arr[j] > key) {
                arr[j + 1] = arr[j]; // 后面的元素依次向前移动
                j--; // 移动前一个元素
            }

            // 插入key元素到正确的位置
            arr[j + 1] = key;
        }
    }
}

详细讲解:

  1. 整体流程:插入排序的思想是将数组分为已排序部分和未排序部分,初始时已排序部分只有第一个元素。对于未排序部分的每一个元素,都把它插入到已排序部分的适当位置,从而保持已排序部分始终有序。
  2. 外层循环:初始化为 for (int i = 1; i < n; i++),这里 n 是数组长度。每次循环都会处理未排序部分的一个元素(从第二个元素开始)。
  3. 提取待插入元素:首先,将当前未排序元素的值存储在变量 key 中,即 int key = arr[i];。
  4. 查找插入位置:通过内层循环 while (j >= 0 && arr[j] > key),从已排序部分的末尾开始向前遍历,直到找到一个比 key 小或等于的元素。这相当于模拟将 key 插入到已排序部分的过程。
  5. 元素移动:在内层循环中,如果找到一个比 key 大的元素,则将这个元素向右移动一位,即将 arr[j] 的值赋给 arr[j + 1]。这样反复进行,直到找到插入位置或者遍历完已排序部分。
  6. 插入元素:退出内层循环后,将 key 插入到正确的位置,即 arr[j + 1] = key;。此时,j + 1 位置之前的元素都小于等于 key。
  7. 重复过程:随着外层循环的进行,每个未排序元素都会被插入到已排序部分的合适位置。当所有元素都被处理过后,整个数组便按照升序排列。

插入排序的时间复杂度在最好情况下(即输入数组已经是有序的)为 O(n),最坏情况下(即输入数组是逆序的)和平均情况下为 O(n^2)。对于小规模数据或部分有序数据,插入排序相对高效。

四、希尔排序(Shell Sort)

Java希尔排序(Shell Sort)示例代码:

public class ShellSortExample {
    public static void main(String[] args) {
        int[] arr = {9, 5, 2, 7, 1, 6, 3, 8, 4}; // 待排序的数组
        shellSort(arr); // 调用希尔排序方法
        System.out.println("Sorted array:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }

    public static void shellSort(int[] arr) {
        int len = arr.length;
        // 希尔排序的增量序列,这里采用Hibbard增量序列
        for (int gap = len / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < len; i++) {
                int temp = arr[i];
                int j;
                
                // 插入排序,将temp元素插入到gap大小的有序子序列中
                for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
                    arr[j] = arr[j - gap];
                }
                arr[j] = temp;
            }
        }
    }
}

详细讲解:
希尔排序(Shell Sort)是由Donald Shell于1959年提出的一种改进的插入排序算法。它通过定义一个称为“增量”的值,将数组分割成几个子序列,然后对每个子序列进行插入排序。随着增量逐渐减小,子序列越来越短,直到增量为1时,相当于进行了完整的插入排序,此时数组已基本有序。

  1. 增量序列选择: 希尔排序的效率很大程度上取决于增量序列的选择。在示例代码中,我们采用了Hibbard增量序列,它是按照gap = gap / 2的规律递减的,例如:len/2, len/4, len/8, ... , 1。
  2. 插入排序改进: 在外层循环中,针对每个增量gap,我们会对数组进行分组,每组之间隔开gap个元素。然后对每个组内的元素进行插入排序。这里插入排序的逻辑是在内层循环中实现的。
  3. 插入排序过程: 对于内层循环中的每个元素arr[i],它会被逐渐向前移动,直到找到一个应该插入的位置。这个过程就是典型的插入排序过程,只不过这里的移动跨度是gap而非每次只移动一个位置。
  4. 重复过程: 当所有以当前gap为间隔的子序列都完成插入排序后,继续减少gap的值,直到gap变为1,这时的插入排序就变成了常规的插入排序,而由于之前已将数据大致整理成有序的状态,最后的插入排序就能更快完成排序任务。

希尔排序的时间复杂度并不容易精确计算,因为它依赖于增量序列的选择。通常来说,希尔排序在实际应用中表现良好,尤其对于大规模数据集,其效率要比简单插入排序高得多。不过,希尔排序并不能保证在所有情况下都能达到线性时间复杂度,最好的情况下的时间复杂度是介于O(n)至O(n^1.5)之间。

五、归并排序(Merge Sort)

Java归并排序(Merge Sort)示例代码:

import java.util.Arrays;

public class MergeSortExample {

    // 归并排序方法
    public static void mergeSort(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }

        // 使用辅助数组进行归并操作
        int[] temp = new int[arr.length];
        mergeSort(arr, temp, 0, arr.length - 1);
    }

    // 递归的归并排序实现
    private static void mergeSort(int[] arr, int[] temp, int left, int right) {
        if (left < right) {
            int mid = (left + right) / 2;
            
            // 递归地对左半部分进行归并排序
            mergeSort(arr, temp, left, mid);
            
            // 递归地对右半部分进行归并排序
            mergeSort(arr, temp, mid + 1, right);

            // 合并两个有序的子数组
            merge(arr, temp, left, mid, right);
        }
    }

    // 合并两个有序数组
    private static void merge(int[] arr, int[] temp, int left, int mid, int right) {
        // 左半部分的长度
        int leftLength = mid - left + 1;
        // 右半部分的长度
        int rightLength = right - mid;

        // 复制左半部分和右半部分到临时数组
        System.arraycopy(arr, left, temp, 0, leftLength);
        System.arraycopy(arr, mid + 1, temp, leftLength, rightLength);

        // 定义两个指针,分别指向左右两个子数组的起始位置
        int i = 0, j = leftLength, k = left;

        // 从左、右两个子数组中挑选出较小的元素放入原数组中
        while (i < leftLength && j < rightLength) {
            if (temp[i] <= temp[j]) {
                arr[k++] = temp[i++];
            } else {
                arr[k++] = temp[j++];
            }
        }

        // 若左半部分还有剩余元素,则直接复制到原数组中
        while (i < leftLength) {
            arr[k++] = temp[i++];
        }

        // 若右半部分还有剩余元素,则直接复制到原数组中
        while (j < rightLength) {
            arr[k++] = temp[j++];
        }
    }

    public static void main(String[] args) {
        int[] arr = {9, 5, 2, 7, 1, 6, 3, 8, 4};
        mergeSort(arr);
        System.out.println("Sorted array: " + Arrays.toString(arr));
    }
}

详细讲解:

  1. 基本概念: 归并排序是一种基于分治策略的排序算法,它将一个大问题分解为多个相同的小问题,解决小问题后,再将结果合并,从而得到原问题的解决方案。在排序数组的过程中,归并排序不断地将数组切分为两半,对每一半进行排序,最后将两个已排序的半部分合并成一个完整的有序数组。
  2. 递归过程: 在mergeSort方法中,首先检查数组长度,若数组为空或只有一个元素则无需排序。然后,创建一个与原数组等长的临时数组,用于存储中间结果。接下来,递归地对数组的左右两半分别进行归并排序。
  3. 合并操作: 在merge方法中,首先将左右两个已排序的子数组复制到临时数组中。然后,定义三个指针分别指向临时数组的左右起点以及原数组的当前填充位置。通过比较临时数组中左右子数组的当前元素,将较小的元素放入原数组中,直到某一边的子数组全部被处理完毕,再将另一边剩余的元素全部拷贝回原数组。
  4. 时间复杂度: 归并排序的合并操作的时间复杂度为O(n),其中n为数组长度。由于每次将问题规模缩小一半,所以总的合并操作次数约为log₂n,因此归并排序的总时间复杂度为O(n log n),无论在最好、最坏还是平均情况下,均维持这一时间复杂度。同时,归并排序的空间复杂度为O(n),因为需要额外的存储空间来执行合并操作。

六、快速排序(Quick Sort)

Java快速排序(Quick Sort)示例代码:

public class QuickSortExample {
    
    // 快速排序方法
    public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            // 找到划分的基准元素的正确位置
            int pivotIndex = partition(arr, low, high);
            
            // 递归地对基准元素左边的子数组进行排序
            quickSort(arr, low, pivotIndex - 1);
            
            // 递归地对基准元素右边的子数组进行排序
            quickSort(arr, pivotIndex + 1, high);
        }
    }

    // 划分数组的方法
    private static int partition(int[] arr, int low, int high) {
        // 选择最后一个元素作为基准元素
        int pivot = arr[high];

        // 初始化两个指针,i指向low,j指向low-1
        int i = (low - 1);
        
        // 遍历low到high之间的元素
        for (int j = low; j < high; j++) {
            // 如果当前元素小于或等于基准元素,则将i右侧的元素向左移动并将当前元素放置在i+1的位置
            if (arr[j] <= pivot) {
                i++;

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

        // 最后,将基准元素和i+1位置的元素交换
        int temp = arr[i + 1];
        arr[i + 1] = arr[high];
        arr[high] = temp;

        // 返回基准元素的新位置
        return i + 1;
    }

    public static void main(String[] args) {
        int[] arr = {9, 5, 2, 7, 1, 6, 3, 8, 4};
        int n = arr.length;
        quickSort(arr, 0, n - 1);
        System.out.println("Sorted array: " + Arrays.toString(arr));
    }
}

详细讲解:

  1. 快速排序的基本思想: 快速排序是一种高效的排序算法,基于分治策略。它选择一个基准元素,将数组划分为两个子数组:一个包含所有小于基准元素的元素,另一个包含所有大于基准元素的元素。然后递归地对这两个子数组进行快速排序,直到子数组只剩下一个元素,整个数组便被排序好了。
  2. 划分操作(partition): 在partition方法中,首先选择最后一个元素作为基准元素。初始化两个指针,i 指向 low - 1,j 从 low 开始遍历数组。如果遇到小于等于基准元素的值,就将 i 右侧的元素向左移动,然后将 j 位置的元素放到 i + 1 位置。这样做的目的是将小于基准元素的所有值集中到基准元素左侧,大于基准元素的值集中在右侧。遍历完成后,将基准元素与 i + 1 位置的元素交换,这样基准元素就到了正确的位置。
  3. 递归调用: 在 quickSort 方法中,首先判断是否有需要排序的区域(即 low < high),如果有,则调用 partition 方法找出基准元素的位置,并以基准元素为界,分别对基准元素左边和右边的子数组进行递归排序。
  4. 时间复杂度: 在理想情况下,快速排序的平均时间复杂度为 O(n log n),这是因为每次划分都能将问题规模约减一半,然后再进行一次线性扫描。在最坏的情况下(例如,当输入数组已经完全有序或完全逆序时),快速排序的时间复杂度可能会退化到 O(n²),但通过随机化选择基准元素或者采用三数取中法等策略,可以极大地降低这种情况发生的概率。
  5. 空间复杂度: 快速排序的空间复杂度为 O(log n),这是由于递归调用所需的栈空间。另外,在原地排序版本中,不需要额外的存储空间,空间复杂度可以降低到 O(1)。

七、堆排序(Heap Sort)

Java堆排序(Heap Sort)示例代码:

public class HeapSortExample {
    public static void main(String[] args) {
        int[] arr = {9, 5, 2, 7, 1, 6, 3, 8, 4}; // 待排序的数组
        buildMaxHeap(arr); // 创建大顶堆
        heapSort(arr); // 调用堆排序方法
        System.out.println("Sorted array:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }

    // 创建大顶堆
    public static void buildMaxHeap(int[] arr) {
        int n = arr.length;
        // 从最后一个非叶子节点开始向上调整堆
        for (int i = (n / 2) - 1; i >= 0; i--) {
            maxHeapify(arr, n, i);
        }
    }

    // 调整大顶堆
    private static void maxHeapify(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);
            maxHeapify(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;

        // 先构建大顶堆
        buildMaxHeap(arr);

        // 依次取出堆顶元素(最大值),交换到末尾,并重新调整堆
        for (int i = n - 1; i > 0; i--) {
            swap(arr, 0, i);
            maxHeapify(arr, i, 0);
        }
    }
}

// 下面是完整代码块,包含了上述提到的方法

详细讲解:

  1. 堆排序简介: 堆排序是一种基于比较的排序算法,它利用了堆这种数据结构,具体来说,是通过构建一个大顶堆(父节点的值总是大于或等于其子节点的值)。堆排序分为两步:构建堆和堆调整。
  2. 构建大顶堆: buildMaxHeap 函数首先从最后一个非叶子节点(即最后一个元素的父节点)开始,自底向上地对每个非叶子节点进行 maxHeapify 调整,确保每个子树都满足大顶堆的性质。
  3. 调整大顶堆(maxHeapify): maxHeapify 函数接收一个数组、数组长度以及当前要检查的节点索引,首先假设当前节点是最大值,然后分别比较它的左孩子和右孩子,如果发现更大的节点,则更新最大值索引。如果最大值不是当前节点,就交换当前节点和最大值所在节点的内容,然后对新形成的子树继续进行 maxHeapify 调整。
  4. 堆排序过程: heapSort 函数首先调用 buildMaxHeap 构建初始的大顶堆。然后,从堆顶(即数组的第一个元素,同时也是当前的最大值)开始,将其与数组的最后一个元素交换,之后减小堆大小并再次调用 maxHeapify 修复堆的性质,使得剩余的元素中,堆顶仍然是最大的。重复这个过程,直至整个数组有序。
  5. 时间复杂度: 堆排序的时间复杂度为 O(n log n),其中 n 为数组长度。构建初始堆的过程大约需要 O(n) 时间,而对 n-1 个元素进行堆调整的过程需要 O(log n) 次比较和交换,因此总时间复杂度为 O(n log n)。堆排序的空间复杂度为 O(1),因为它可以在原地进行排序,不需额外空间。

八、计数排序(Counting Sort)

Java计数排序(Counting Sort)示例代码:

public class CountingSortExample {

    public static void countingSort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }

        // 寻找数组中的最大值
        int maxVal = Arrays.stream(arr).max().getAsInt();

        // 创建一个计数数组,长度为 maxVal + 1
        int[] countArray = new int[maxVal + 1];

        // 统计每个元素的出现次数
        for (int num : arr) {
            countArray[num]++;
        }

        // 计算每个元素的累计计数,得到排序后的索引
        for (int i = 1; i < countArray.length; i++) {
            countArray[i] += countArray[i - 1];
        }

        // 创建一个新的临时数组存放排序后的结果
        int[] sortedArr = new int[arr.length];
        // 根据计数数组将元素按顺序放入新的数组
        for (int i = arr.length - 1; i >= 0; i--) {
            sortedArr[countArray[arr[i]] - 1] = arr[i];
            countArray[arr[i]]--; // 更新计数数组,防止重复计数
        }

        // 将排序好的元素复制回原数组
        System.arraycopy(sortedArr, 0, arr, 0, arr.length);
    }

    public static void main(String[] args) {
        int[] arr = {9, 5, 2, 7, 1, 6, 3, 8, 4};
        countingSort(arr);
        System.out.println("Sorted array: " + Arrays.toString(arr));
    }
}
public class CountingSortExample {

    public static void countingSort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }

        // 寻找数组中的最大值
        int maxVal = Arrays.stream(arr).max().getAsInt();

        // 创建一个计数数组,长度为 maxVal + 1
        int[] countArray = new int[maxVal + 1];

        // 统计每个元素的出现次数
        for (int num : arr) {
            countArray[num]++;
        }

        // 计算每个元素的累计计数,得到排序后的索引
        for (int i = 1; i < countArray.length; i++) {
            countArray[i] += countArray[i - 1];
        }

        // 创建一个新的临时数组存放排序后的结果
        int[] sortedArr = new int[arr.length];
        // 根据计数数组将元素按顺序放入新的数组
        for (int i = arr.length - 1; i >= 0; i--) {
            sortedArr[countArray[arr[i]] - 1] = arr[i];
            countArray[arr[i]]--; // 更新计数数组,防止重复计数
        }

        // 将排序好的元素复制回原数组
        System.arraycopy(sortedArr, 0, arr, 0, arr.length);
    }

    public static void main(String[] args) {
        int[] arr = {9, 5, 2, 7, 1, 6, 3, 8, 4};
        countingSort(arr);
        System.out.println("Sorted array: " + Arrays.toString(arr));
    }
}

详细讲解:

  • 原理: 计数排序是一种非比较型整数排序算法,其基本思想是对每一个输入元素x,确定出不大于x的元素个数,由此可以直接把x放到它在输出数组中的位置上。算法的核心在于统计每个数字出现的次数,形成一个计数数组。
  • 步骤:
    • 找出数组中的最大值:首先需要找到待排序数组中的最大值,以便确定计数数组的长度。
    • 创建并填充计数数组:创建一个长度为最大值加一的计数数组,遍历原数组,每当遇到某个数时,对应的计数数组位置上的计数值就增加1,这样计数数组的每个位置就代表了对应数值出现的次数。
    • 累加计数数组:从第二个元素开始,将每个元素的计数值累加上前一个元素的计数值,这样得到的数组,其第i个位置的值就表示不大于i的元素总共有多少个。
    • 填充值到新数组:从原数组的最后一个元素开始,利用计数数组将元素按照它们的最终位置放入新的临时数组。具体做法是先查看计数数组中当前元素的计数值,将其作为新数组的索引,然后将原数组的当前元素放入新数组的相应位置,并将计数数组中的对应计数值减1,以避免重复计数。
    • 复制排序后的数组:最后,将排序好的临时数组复制回原数组,完成排序。
  • 适用场景: 计数排序特别适合于待排序数据范围不是很大并且数据分布均匀的情况,尤其是当数据范围远小于待排序元素的数量时,计数排序具有较高的效率,时间复杂度为O(n+k),其中n是待排序元素的数量,k是数据的范围。然而,当k接近或大于n时,这种方法就不再高效,因为会消耗大量的内存。

九、桶排序(Bucket Sort)

Java桶排序(Bucket Sort)示例代码:

import java.util.ArrayList;
import java.util.List;

public class BucketSortExample {

    public static void bucketSort(double[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }

        // 获取数组中的最大值和最小值
        double minValue = arr[0], maxValue = arr[0];
        for (double value : arr) {
            minValue = Math.min(minValue, value);
            maxValue = Math.max(maxValue, value);
        }

        // 计算桶的数量
        int bucketCount = 10; // 这里假设我们想要10个桶,根据实际情况应动态计算
        double range = maxValue - minValue;
        double bucketSize = range / bucketCount;

        // 创建桶列表
        List<Double>[] buckets = new ArrayList[bucketCount];
        for (int i = 0; i < buckets.length; i++) {
            buckets[i] = new ArrayList<>();
        }

        // 将元素分配到各个桶中
        for (double num : arr) {
            int index = (int) ((num - minValue) / bucketSize);
            buckets[index].add(num);
        }

        // 对每个桶进行排序(这里假设使用插入排序,根据具体情况可以选择其他排序算法)
        for (List<Double> bucket : buckets) {
            if (bucket != null && !bucket.isEmpty()) {
                Collections.sort(bucket);
            }
        }

        // 将排序后的桶中的元素合并回原数组
        int idx = 0;
        for (List<Double> bucket : buckets) {
            if (bucket != null) {
                for (double value : bucket) {
                    arr[idx++] = value;
                }
            }
        }
    }

    public static void main(String[] args) {
        double[] arr = {0.2, 0.8, 0.1, 0.5, 0.3, 0.7, 0.4, 0.6};
        bucketSort(arr);
        System.out.println("Sorted array: " + Arrays.toString(arr));
    }
}

详细讲解:

  • 原理: 桶排序是一种分布式排序算法,它将待排序的数据分配到有限数量的桶中,然后对每个桶分别进行排序(可以使用任何合适的内部排序算法)。最后,将所有桶中的数据按照顺序连接起来,得到最终排序的结果。
  • 步骤:
    • 预处理:首先获取数组中的最大值和最小值,从而计算出数据的范围。
    • 分配:根据指定的桶数量和数据范围,确定每个桶的宽度(桶大小),然后遍历原始数组,将每个元素放入相应的桶中。通常,可以通过 (value - minValue) / bucketSize 的方式计算元素应该进入哪个桶。
    • 排序桶:对每个非空桶独立进行排序。在上面的示例代码中,使用的是Java集合框架中的Collections.sort()方法对每个桶进行插入排序。
    • 收集结果:将各个桶中的元素按照桶的顺序依次取出,合并到结果数组中,得到最终排序的数组。
  • 性能分析: 桶排序的时间复杂度在理想情况下能够达到O(n + k),其中n是待排序数据的数量,k是桶的数量。但是,当数据分布不均匀时,桶排序的效率可能受到影响。此外,每个桶内部排序的算法也会对整体性能产生影响。如果桶内数据量较少,使用简单的排序算法如插入排序即可;但如果桶内数据量较大,则需要更高效的排序算法。

十、基数排序(Radix Sort)

Java基数排序(Radix Sort)示例代码:

import java.util.Arrays;

public class RadixSortExample {

    // 基数排序函数
    public static void radixSort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }

        // 找到数组中的最大值,以确定需要处理的最高位数
        int maxVal = Arrays.stream(arr).max().getAsInt();
        int maxDigits = (int)Math.floor(Math.log10(maxVal)) + 1;

        // 从最低有效位(LSD)开始,对每一位进行排序
        for (int digit = 0; digit < maxDigits; digit++) {
            // 使用计数排序作为基础排序算法
            countSort(arr, digit);
        }
    }

    // 计数排序函数,用于对特定位进行排序
    private static void countSort(int[] arr, int digitPosition) {
        int[] output = new int[arr.length];
        int[] count = new int[10];

        // 初始化计数数组
        Arrays.fill(count, 0);

        // 统计各桶中的元素数量
        for (int num : arr) {
            int digit = getDigit(num, digitPosition);
            count[digit]++;
        }

        // 将计数数组转换为累积计数
        for (int i = 1; i < 10; i++) {
            count[i] += count[i - 1];
        }

        // 从后往前遍历原数组,根据统计数组中的位置将元素放入输出数组
        for (int i = arr.length - 1; i >= 0; i--) {
            int digit = getDigit(arr[i], digitPosition);
            output[count[digit] - 1] = arr[i];
            count[digit]--;
        }

        // 将排序后的元素复制回原数组
        System.arraycopy(output, 0, arr, 0, arr.length);
    }

    // 获取给定数在指定位置上的数字
    private static int getDigit(int number, int position) {
        return (number / (int)Math.pow(10, position)) % 10;
    }

    public static void main(String[] args) {
        int[] arr = {93, 21, 54, 72, 35, 10, 63, 81, 27, 45};
        radixSort(arr);
        System.out.println("Sorted array: " + Arrays.toString(arr));
    }
}

详细讲解:

  • 基数排序的基本原理: 基数排序是一种非比较型整数排序算法,它的工作原理是将整数按位数切割成不同的“桶”,然后按每个位数分别比较。从最低有效位(LSD)开始,对每一位进行排序,直到最高有效位(MSD)。每一轮排序针对一位数字,使用稳定的排序算法(如计数排序)对这些数字进行排序。
  • 步骤:
    • 确定最大值:首先找到待排序数组中的最大值,以便知道需要处理多少位数字。
    • 循环处理每一位:对于每一位数字,使用计数排序对该位上的数字进行排序。
    • 使用计数排序:在countSort方法中,对每一位上的数字进行统计,并建立映射关系。然后,根据映射表将原数组中的元素放入正确的顺序,放入输出数组中。
    • 获取指定位置的数字:getDigit方法用于获取一个整数在特定位置上的数字,这通过除以适当的幂次方和取模运算来实现。
  • 时间复杂度: 基数排序的时间复杂度是O(kn),其中k是数组中最大数字的位数,n是数组的长度。在实际应用中,k相对于n往往很小,因此基数排序在处理大量整数排序时效率很高。

总结

在实际开发中,Java的Arrays.sort()方法底层实现了高度优化的TimSort算法,结合了归并排序和插入排序的优点,适用于大多数通用排序场景。尽管如此,理解和掌握各类排序算法仍然是提高算法思维和优化程序性能的重要手段。开发者应根据实际需求,结合数据特性和规模,灵活选择并应用相应的排序算法。

  • 44
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小码快撩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值