算法之旅(五)基础算法 排序算法和查找算法

常用的排序算法和查找算法

在计算机科学中,排序算法查找算法是两类最基本、最常用的算法。

  • 排序算法用于将一组数据按照某种顺序(如升序、降序)进行排列;
  • 查找算法用于在数据集合中寻找满足特定条件的元素。

一、排序算法

常用的排序算法:冒泡排序、选择排序、快速排序

冒泡排序(Bubble Sort)

1. 算法描述

冒泡排序是一种简单直观的排序算法。它重复地遍历待排序的序列,每次比较相邻的两个元素,如果它们的顺序错误就交换它们。这个过程会将较大的元素逐渐“冒泡”到序列的末尾。

demo code

public static void bubbleSort(int[] arr) {
    int n = arr.length;
    boolean swapped;
    // 外层循环控制遍历次数
    for (int i = 0; i < n - 1; i++) {
        swapped = false;
        // 内层循环进行相邻元素比较
        for (int j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换 arr[j] 和 arr[j + 1]
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swapped = true;
            }
        }
        // 如果没有发生交换,说明序列已排序,提前结束
        if (!swapped) break;
    }
}

时间和空间复杂度

  • 时间复杂度
    • 最优情况:O(n)(当序列已经有序时)
    • 最坏情况:O(n²)
    • 平均情况:O(n²)
  • 空间复杂度:O(1)(原地排序,只需常数级额外空间)
  • 稳定性:稳定(相等元素的相对顺序不变)

二、选择排序(Selection Sort)

1. 算法描述

选择排序是一种简单的排序算法。它的基本思想是:每一轮从未排序的部分中选出最小(或最大)的元素,放到已排序部分的末尾,直到所有元素都被排序。

2. demo code

public static void selectionSort(int[] arr) {
    int n = arr.length;
    // 一共进行 n - 1 轮选择
    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;
        }
    }
}

3. 时间和空间复杂度

  • 时间复杂度
    • 最优、最坏、平均情况:O(n²)
  • 空间复杂度:O(1)
  • 稳定性:不稳定(交换可能改变相等元素的相对顺序)

三、快速排序(Quick Sort)

1. 算法描述

快速排序是对冒泡排序的一种改进,是一种高效的排序算法。它采用了分治的思想,通过选取一个基准元素(pivot),将序列划分为两部分:左侧部分小于基准,右侧部分大于基准。然后递归地对左右两部分进行排序。

2. 快速排序对冒泡排序的优化

  • 减少比较次数:冒泡排序每次只交换相邻的两个元素,而快速排序通过分区,使得每次可以确定一个元素的最终位置,减少了不必要的比较和交换。
  • 采用分治策略:快速排序将问题分解成较小的子问题,递归地解决,提高了效率。
  • 平均时间复杂度更低:快速排序的平均时间复杂度为 O(n log n),比冒泡排序的 O(n²) 更优。

3. demo code

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];
    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;
        }
    }
    // 将基准元素放到正确的位置
    int temp = arr[i + 1];
    arr[i + 1] = arr[high];
    arr[high] = temp;
    return i + 1;
}

注意:调用 quickSort 方法时,初始参数应为 quickSort(arr, 0, arr.length - 1);

4. 时间和空间复杂度

  • 时间复杂度
    • 最优情况:O(n log n)(每次均匀划分)
    • 最坏情况:O(n²)(每次划分不均匀,如已排序的序列)
    • 平均情况:O(n log n)
  • 空间复杂度
    • 平均情况:O(log n)(递归调用栈的深度)
    • 最坏情况:O(n)
  • 稳定性:不稳定(交换可能改变相等元素的相对顺序)

四、三种排序算法的对比

排序算法 最佳时间复杂度 最差时间复杂度 平均时间复杂度 空间复杂度 稳定性
冒泡排序 O(n) O(n²) O(n²) O(1) 稳定
选择排序 O(n²) O(n²) O(n²) O(1) 不稳定
快速排序 O(n log n) O(n²) O(n log n) O(log n) 不稳定
  • 效率比较

    • 冒泡排序选择排序的时间复杂度为 O(n²),适合小规模数据排序。
    • 快速排序的平均时间复杂度为 O(n log n),比前两者更高效,适合大规模数据排序。
  • 空间复杂度

    • 冒泡排序和选择排序的空间复杂度为 O(1),是原地排序算法。
    • 快速排序的空间复杂度取决于递归调用栈的深度,平均为 O(log n)。
  • 稳定性

    • 冒泡排序是稳定的,选择排序和快速排序是不稳定的。

冒泡排序与快速排序的关系

  • 思想上的关联:冒泡排序和快速排序都通过比较和交换元素来实现排序,但冒泡排序每次只交换相邻的两个元素,而快速排序通过分区的方式一次定位一个元素的最终位置。
  • 优化方向:快速排序通过引入分治策略,减少了不必要的比较和交换,克服了冒泡排序效率低下的缺点。

2. 选择合适的排序算法

  • 小规模数据或数据基本有序:可以选择冒泡排序或插入排序。
  • 需要稳定排序:选择冒泡排序。
  • 大规模数据:选择快速排序或其他时间复杂度为 O(n log n) 的算法(如归并排序、堆排序)。
  • 空间受限:选择空间复杂度为 O(1) 的原地排序算法,如快速排序(需要注意最坏情况下的空间复杂度)。

查找算法

1. 线性查找(Linear Search)

算法描述

线性查找从数组的第一个元素开始,依次比较每个元素,直到找到目标值或遍历完整个数组。

Demo code

public static int linearSearch(int[] arr, int target) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == target) return i;
    }
    return -1; // 未找到
}

时间和空间复杂度

  • 时间复杂度
    • 最佳情况:O(1)(目标值在第一个位置)
    • 最差情况:O(n)
    • 平均情况:O(n)
  • 空间复杂度:O(1)

2. 二分查找(Binary Search)

算法描述

二分查找要求数组是有序的。它通过比较目标值与中间元素的大小,决定在左半部分还是右半部分继续查找,递归或迭代地缩小查找范围。

demo code

public static int binarySearch(int[] arr, int target) {
    int low = 0, high = arr.length - 1;
    while (low <= high) {
        int mid = low + (high - low) / 2; // 防止溢出
        if (arr[mid] == target) return mid;
        else if (arr[mid] < target) low = mid + 1;
        else high = mid - 1;
    }
    return -1; // 未找到
}

时间和空间复杂度

  • 时间复杂度
    • 最佳情况:O(1)(目标值在中间位置)
    • 最差情况:O(log n)
    • 平均情况:O(log n)
  • 空间复杂度
    • 迭代版本:O(1)
    • 递归版本:O(log n)(递归调用栈的深度)

3. 深度优先搜索(Depth-First Search, DFS)

算法描述

DFS 常用于图或树的遍历,从起始节点开始,尽可能深入地访问邻接节点,直到所有可达节点都被访问。

demo code

// 假设使用邻接表表示图
public static void dfs(int node, boolean[] visited, List<List<Integer>> graph) {
    visited[node] = true;
    System.out.print(node + " ");
    for (int neighbor : graph.get(node)) {
        if (!visited[neighbor]) {
            dfs(neighbor, visited, graph);
        }
    }
}

时间和空间复杂度

  • 时间复杂度:O(V + E)
    • V:节点数
    • E:边数
  • 空间复杂度:O(V)(递归调用栈和访问标记数组)

4. 广度优先搜索(Breadth-First Search, BFS)

算法描述

BFS 从起始节点开始,按层次逐步访问邻接节点,使用队列维护待访问的节点列表。

demo code

public static void bfs(int startNode, List<List<Integer>> graph) {
    boolean[] visited = new boolean[graph.size()];
    Queue<Integer> queue = new LinkedList<>();
    visited[startNode] = true;
    queue.offer(startNode);

    while (!queue.isEmpty()) {
        int node = queue.poll();
        System.out.print(node + " ");
        for (int neighbor : graph.get(node)) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                queue.offer(neighbor);
            }
        }
    }
}

时间和空间复杂度

  • 时间复杂度:O(V + E)
  • 空间复杂度:O(V)(队列和访问标记数组)

5. 插值查找(Interpolation Search)

算法描述

插值查找是改进的二分查找,假设数据是均匀分布的,通过估计目标值的位置,可能比二分查找更快。

demo code

public static int interpolationSearch(int[] arr, int target) {
    int low = 0, high = arr.length - 1;
    while (low <= high && target >= arr[low] && target <= arr[high]) {
        if (low == high) {
            if (arr[low] == target) return low;
            else return -1;
        }
        int pos = low + ((target - arr[low]) * (high - low)) / (arr[high] - arr[low]);
        if (arr[pos] == target) return pos;
        else if (arr[pos] < target) low = pos + 1;
        else high = pos - 1;
    }
    return -1;
}

时间和空间复杂度

  • 时间复杂度
    • 最佳情况:O(1)
    • 最差情况:O(n)
    • 平均情况:O(log log n)(当数据均匀分布时)
  • 空间复杂度:O(1)

排序算法对比

排序算法 最佳时间复杂度 最差时间复杂度 平均时间复杂度 空间复杂度 稳定性
冒泡排序 O(n) O(n²) O(n²) O(1) 稳定
选择排序 O(n²) O(n²) O(n²) O(1) 不稳定
插入排序 O(n) O(n²) O(n²) O(1) 稳定
归并排序 O(n log n) O(n log n) O(n log n) O(n) 稳定
快速排序 O(n log n) O(n²) O(n log n) O(log n) 不稳定
堆排序 O(n log n) O(n log n) O(n log n) O(1) 不稳定

查找算法对比

查找算法 最佳时间复杂度 最差时间复杂度 平均时间复杂度 空间复杂度
线性查找 O(1) O(n) O(n) O(1)
二分查找 O(1) O(log n) O(log n) O(1)
深度优先搜索 O(1) O(V + E) O(V + E) O(V)
广度优先搜索 O(1) O(V + E) O(V + E) O(V)
插值查找 O(1) O(n) O(log log n) O(1)

结语

掌握常用的排序算法和查找算法,对于解决实际问题和优化程序性能至关重要。在选择算法时,应根据具体的应用场景、数据规模和对性能的要求,综合考虑算法的时间和空间复杂度。

  • 小规模数据或基本有序的数据:可以选择简单的排序算法,如插入排序。
  • 需要稳定排序:选择归并排序或冒泡排序。
  • 大规模数据:选择时间复杂度为 O(n log n) 的排序算法,如快速排序、归并排序或堆排序。
  • 数据有序且需要高效查找:使用二分查找。
  • 数据分布均匀:插值查找可能比二分查找更高效。

在实际开发中,还可以利用编程语言提供的库函数,如 Arrays.sort()Collections.sort(),这些方法经过高度优化,能够满足大多数排序需求。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。

相关阅读更多精彩内容

友情链接更多精彩内容