数据结构与算法

最近面试经常遇到的一些算法题,总结一下。

常见的排序算法

2025.2.19腾讯一面、2025.2.21字节一面

快速排序

算法思路分析

方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从找一个小于6的数,再从找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即=10),指向数字。

首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。

现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下:

6 1 2 5 9 3 4 7 10 8

到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下:

6 1 2 5 4 3 9 7 10 8

第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下:

3 1 2 5 4 6 9 7 10 8

到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。之后以6为边界,递归地去“探测”左右子数组,这样就是快速排序的全过程了。

时间复杂度分析

时间复杂度最好情况nlog(n)

最坏情况:log(n2)

对应场景:

  • 场景:每次分区后一个子数组长度为 0,另一个为 n−1(例如数组已有序,且始终选择第一个元素为基准)

具体代码实现

    //快速排序核心思路
    //过选择一个基准元素(这里是数组的第一个元素),将数组分为两部分:一部分小于基准元素,
    // 另一部分大于基准元素,然后递归地对这两部分进行排序。
    public static void quickSort(int[] arr,int low,int high){
        int i,j,temp,t;
        if(low>high){return;}
        //i 从左向右移动,寻找大于基准元素的元素。
        i=low;
        //j 从右向左移动,寻找小于基准元素的元素。
        j=high;
        //temp就是基准位,选择当前数组分段的第一个元素
        temp = arr[low];
        while (i<j) {
            //先看右边,依次往左递减
            while (temp<=arr[j]&&i<j) {
                j--;
            }
            //再看左边,依次往右递增
            while (temp>=arr[i]&&i<j) {
                i++;
            }
            //如果满足条件则交换
            if (i<j) {
                t = arr[j];
                arr[j] = arr[i];
                arr[i] = t;
            }
        }
        //最后将基准数与i和j相等位置的数字交换
        arr[low] = arr[i];
        arr[i] = temp;
        //递归调用左半数组
        quickSort(arr, low, j-1);
        //递归调用右半数组
        quickSort(arr, j+1, high);
    }
    public static void main(String[] args) {
        int[] arr = {10, 7, 8, 9, 1, 5,1};
        System.out.println("排序前: " + Arrays.toString(arr));
        quickSort(arr, 0, arr.length - 1);
        System.out.println("排序后: " + Arrays.toString(arr));
    }

堆排序

算法思路分析

什么是堆

堆是一种叫做完全二叉树的数据结构,可以分为大根堆,小根堆,而堆排序就是基于这种结构而产生的一种程序算法。

堆的分类

大根堆:每个节点的值都大于或者等于他的左右孩子节点的值

小根堆:每个结点的值都小于或等于其左孩子和右孩子结点的值

两种结构映射到数组为:

大根堆:

小根堆

映射关系如下:

父节点下标i--->左孩子:2*i+1, 右孩子:2*i+2

子节点下标i--->(i-1)/2 (注意得到的结果向下取整)

排序思路

1.首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端

2.将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1

3.将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组

注意:升序用大根堆,降序就用小根堆(默认为升序)

构造堆

构造堆的步骤如下:

首先我们给定一个无序的序列,将其看做一个堆结构,一个没有规则的二叉树,将序列里的值按照从上往下,从左到右依次填充到二叉树中。

构建堆首先要找到最后一个非叶子节点所在的位置,对应的索引为arr.len / 2 -1,对于上图数组长度为5,最后一个非叶子节点为5/2-1=1,即为6这个节点

找到最后一个非叶子节点后,比较它的左右节点中最大的一个的值,是否比他大,如果大就交换位置

在这里5小于6,而9大于6,则交换6和9的位置

找到下一个非叶子节点4,用它和它的左右子节点进行比较,4大于3,而4小于9,交换4和9位置

此时发现4小于5和6这两个子节点,我们需要进行调整,左右节点5和6中,6大于5且6大于父节点4,因此交换4和6的位置

此时我们就构造出来一个大根堆,下来进行排序

排序

首先将顶点元素9与末尾元素4交换位置,此时末尾数字为最大值。排除已经确定的最大元素,将剩下元素重新构建大根堆

一次交换重构如图:

此时元素9已经有序,末尾元素则为4(每调整一次,调整后的尾部元素在下次调整重构时都不能动)

二次交换重构如图:

最终排序结果: 

由此,我们可以归纳出堆排序算法的步骤:

1. 把无序数组构建成二叉堆。

2. 循环删除堆顶元素,移到集合尾部,调节堆产生新的堆顶。

当我们删除一个最大堆的堆顶(并不是完全删除,而是替换到最后面),经过自我调节,第二大的元素就会被交换上来,成为最大堆的新堆顶。

正如上图所示,当我们删除值为9的堆顶节点,经过调节,值为6的新节点就会顶替上来;当我们删除值为6的堆顶节点,经过调节,值为5的新节点就会顶替上来.......

由于二叉堆的这个特性,我们每一次删除旧堆顶,调整后的新堆顶都是大小仅次于旧堆顶的节点。那么我们只要反复删除堆顶,反复调节二叉堆,所得到的集合就成为了一个有序集合,

时间复杂度分析

堆排序是不稳定的排序,空间复杂度为O(1),平均的时间复杂度为O(nlogn),最坏情况下也稳定在O(nlogn) 

具体代码实现

    /*
原理:将数组构建成最大堆(或最小堆),然后逐个取出堆顶元素并调整堆。
时间复杂度:
最好情况:O(n log n)
平均情况:O(n log n)
最坏情况:O(n log n)
空间复杂度:O(1)(原地排序)
稳定性:不稳定
     */
        // 堆排序入口方法
        public static void heapSort(int[] arr) {
            int n = arr.length;
            // 构建最大堆
            for (int i = n / 2 - 1; i >= 0; i--) {
                heapify(arr, n, i);
            }
            // 排序
            for (int i = n - 1; i > 0; i--) {
                // 将堆顶元素(最大值)与堆的最后一个元素交换
                int temp = arr[0];
                arr[0] = arr[i];
                arr[i] = temp;
                // 调整堆,使其重新满足最大堆的性质
                heapify(arr, i, 0);
            }
        }
        // 调整堆
        private static void heapify(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) {
                int temp = arr[i];
                arr[i] = arr[largest];
                arr[largest] = temp;

                // 递归调整子树
                heapify(arr, n, largest);
            }
        }
        // 测试
        public static void main(String[] args) {
            int[] arr = {12, 11, 13, 5, 6, 7};
            System.out.println("排序前: " + Arrays.toString(arr));

            heapSort(arr);

            System.out.println("排序后: " + Arrays.toString(arr));
        }

冒泡排序

算法思路分析

冒泡排序基本思想

         通过对待排序序列从前向后(从下标较小的元素开始),依次对相邻两个元素的值进行两两比较,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就如果水底下的气泡一样逐渐向上冒。

示例分析

  待排序数组:3,9,-1,10,20

       第一轮排序:

        (1)3,9,-1,10,20      ----3跟9比较,不交换

        (2)3,-1,9,10,20      ----9比 -1大,所以9跟 -1交换

        (3)3,-1,9,10,20      ----9跟10比较,不交换

        (4)3,-1,9,10,20      ----10跟20比较,不交换

           第一轮过后,将20这个最大的元素固定到了最后的位置

       第二轮排序:

            因为20的位置已经固定,所以只对前4个进行排序即可:

        (1)-1,3,9,10,20      ----3比 -1大,进行交换

        (2)-1,3,9,10,20      ----3跟9比较,不交换

        (3)-1,3,9,10,20      ----9跟10比较,不交换

            第二轮过后,将第二大的元素固定到了倒数第二个位置

       第三轮排序:

            10和20的位置已经确定,只需对前三个进行排序

        (1)-1,3,9,10,20      ----3和-1比较,不交换

        (2)-1,3,9,10,20      ----3和9比较,不交换

            第三轮过后,将第三大的元素位置确定

       第四轮排序:

            只对前两个元素进行排序

        (1)-1,3,9,10,20      ----3和-1比较,不交换

       第四轮过后,将第四大的元素位置确定,此时总共5个元素,已经排序好4个,从而最后一个自然而然就是排好序的了

时间复杂度分析

最好O(n)输入数组已完全有序1轮扫描,n-1次比较
最坏O(n²)输入数组完全逆序Σ(n-1 + n-2 + ... +1) = n(n-1)/2
平均O(n²)随机排列数组约需要n²/2次比较

具体代码实现

public class BubbleSort {

    public void bubbleSort(int[] arr) {
        if (arr == null || arr.length <= 1) return;
        int n = arr.length;
        
        // 外层循环控制排序轮次(n-1轮)
        for (int i = 0; i < n - 1; i++) {
            boolean swapped = false; // 优化标记
            
            // 内层循环进行相邻元素比较(每轮少比较i个已排序元素)
            for (int j = 0; j < n - 1 - i; j++) {
                // 前 > 后时交换(实现升序排序)
                if (arr[j] > arr[j + 1]) {
                    swap(arr, j, j + 1);
                    swapped = true; // 标记发生交换
                }
            }
            
            // 本轮未发生交换说明已完全有序,提前终止
            if (!swapped) break;
        }
    }

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

    public static void main(String[] args) {
        int[] arr = {5, 2, 9, 1, 5, 6};
        new BubbleSort().bubbleSort(arr);
        System.out.println(Arrays.toString(arr)); // 输出:[1, 2, 5, 5, 6, 9]
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

编程小猹

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

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

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

打赏作者

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

抵扣说明:

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

余额充值