开始刷题吧!十道腾讯金三银四高频leetcode算法题分享(附解题思路及实现代码!)

百度2016研发工程师笔试题解析:C/C++基础与算法考点全拆解 在研发工程师的求职考核中,计算机基础能力始终是衡量人才的核心标尺。从C/C++的指针与内存管理,到数据结构与算法中的数组、链表、二叉树与动态规划,再到操作系统、Linux基础与TCP/HTTP协议,这些看似分散的知识点,实则共同构成了一套完整的底层能力体系。理解数组与指针的差异、堆栈内存的分配原理,以及各类算法的状态转移思想,不仅能帮助开发者快速定位线上性能问题,更能提升在真实工程场景中的综合推理能力。无论是准备大厂笔试的应届生,还是想夯实基础的从业者,都需要从这些核心技术概念出发,掌握其背后的运行机制与工 阅读详情

1.最大子序和

方法一:动态规划

思路和算法

假设 nums 数组的长度是 n,下标从 0 到 n−1。

我们用 f(i) 代表以第 i 个数结尾的「连续子数组的最大和」,那么很显然我们要求的答案就是:

因此我们只需要求出每个位置的 f(i),然后返回 f 数组中的最大值即可。那么我们如何求 f(i) 呢?我们可以考虑 nums[i] 单独成为一段还是加入 f(i−1) 对应的那一段,这取决于 nums[i] 和 f(i−1)+nums[i] 的大小,我们希望获得一个比较大的,于是可以写出这样的动态规划转移方程:

不难给出一个时间复杂度 O(n)、空间复杂度 O(n) 的实现,即用一个 f 数组来保存 f(i) 的值,用一个循环求出所有 f(i)。考虑到 f(i) 只和 f(i−1) 相关,于是我们可以只用一个变量 pre 来维护对于当前 f(i) 的 f(i−1) 的值是多少,从而让空间复杂度降低到 O(1),这有点类似「滚动数组」的思想。

class Solution {
    public int maxSubArray(int[] nums) {
        int pre = 0, maxAns = nums[0];
        for (int x : nums) {
            pre = Math.max(pre + x, x);
            maxAns = Math.max(maxAns, pre);
        }
        return maxAns;
    }
}

复杂度

  • 时间复杂度:O(n),其中 n 为 nums 数组的长度。我们只需要遍历一遍数组即可求得答案。
  • 空间复杂度:O(1)。我们只需要常数空间存放若干变量。

2.10亿个数中如何高效地找到最大的一个数以及最大的第 K 个数

1. 10亿个数中如何高效地找到最大的一个数

将10亿个数据分成1000份,每份100万个数据,找到每份数据中最大的那个数据,最后在剩下的1000个数据里面找出最大的数据。 从100万个数据遍历选择最大的数,此方法需要每次的内存空间为10^6*4=4MB,一共需要1000次这样的比较。

2. 10亿个数中如何高效地找到第K个数

  • 对于top K类问题,通常比较好的方案是分治+hash+小顶堆:
    • 先将数据集按照Hash方法分解成多个小数据集
    • 然后用小顶堆求出每个数据集中最大的K个数
    • 最后在所有top K中求出最终的top K。
  • 如果是top词频可以使用分治+ Trie树/hash +小顶堆:
    • 先将数据集按照Hash方法分解成多个小数据集
    • 然后使用Trie树或者Hash统计每个小数据集中的query词频
    • 之后用小顶堆求出每个数据集中出频率最高的前K个数
    • 最后在所有top K中求出最终的top K。
  • 时间复杂度:建堆时间复杂度是O(K),算法的时间复杂度为O(NlogK)。

3. top K常用的方法

  • 快排+选择排序:排序后的集合中进行查找
    • 时间复杂度: 时间复杂度为O(NlogN)
    • 缺点:需要比较大的内存,且效率低
  • 局部淘汰:取前K个元素并排序,然后依次扫描剩余的元素,插入到排好序的序列中(二分查找),并淘汰最小值。
    • 时间复杂度: 时间复杂度为O(NlogK) (logK为二分查找的复杂度)。
  • 分治法:将10亿个数据分成1000份,每份100万个数据,找到每份数据中最大的K个,最后在剩下的1000*K个数据里面找出最大的K个,100万个数据里面查找最大的K个数据可以使用Partition的方法
    • 时间复杂度: 时间复杂度为O(N+1000*K)
  • Hash法: 如果这10亿个数里面有很多重复的数,先通过Hash法,把这10亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通过分治法或最小堆法查找最大的K个数。
  • 小顶堆: 首先读入前K个数来创建大小为K的小顶堆,建堆的时间复杂度为O(K),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。
    • 时间复杂度: 时间复杂度为O(NlogK)
  • Trie树: 如果是从10亿个重复比较多的单词找高频词汇,数据集按照Hash方法分解成多个小数据集,然后使用Trie树统计每个小数据集中的query词频,之后用小顶堆求出每个数据集中出现频率最高的前K个数,最后在所有top K中求出最终的top K。
    • 适用范围:数据量大,重复多,但是数据种类小可以放入内存
    • 时间复杂度:O(Len*N),N为字符串的个数,Len为字符串长度
  • 桶排序:一个数据表分割成许多buckets,然后每个bucket各自排序,或用不同的排序算法,或者递归的使用bucket sort算法。也是典型的divide-and-conquer分而治之的策略。
    • 使用范围:如果已知了数据的范围,那么可以划分合适大小的桶,直接借用桶排序的思路
    • 时间复杂度:O(N*logM),N 为待排序的元素的个数,M为桶的个数
  • 计数排序:计数排序其实是桶排序的一种特殊情况。当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。
    • 适用范围:只能用在数据范围不大的场景
    • 时间复杂度:O(N)
  • 基数排序:将整数按位数切割成不同的数字,然后按每个位数分别比较。
    • 适用范围:可以对字符串类型的关键字进行排序。
    • 时间复杂度: O(N*M),M为要排序的数据的位数

4. 实际情况

(1)单机+单核+足够大内存

  • 顺序遍历(或先用HashMap求出每个词出现的频率)
    • 查找10亿个查询次(每个占8B)中出现频率最高的10个,考虑到每个查询词占8B,则10亿个查询次所需的内存大约是10^9 * 8B=8GB内存。如果有这么大内存,直接在内存中对查询次进行排序,顺序遍历找出10个出现频率最大的即可。
    • 优点: 简单快速

(2)单机+多核+足够大内存

  • partition + (1)
    • 直接在内存总使用Hash方法将数据划分成n个partition,每个partition交给一个线程处理,线程的处理逻辑同(1)类似,最后一个线程将结果归并。
    • 瓶颈:数据倾斜。每个线程的处理速度可能不同,快的线程需要等待慢的线程。
    • 解决的方法:将数据划分成c×n个partition(c>1),每个线程处理完当前partition后主动取下一个partition继续处理,知道所有数据处理完毕,最后由一个线程进行归并。

(3)单机+单核+受限内存

  • 分治 + (1)
    • 将原文件中的数据切割成M小文件,如果小文件仍大于内存大小,继续采用Hash的方法对数据文件进行分割,直到每个小文件小于内存大小,这样每个文件可放到内存中处理。采用(1)的方法依次处理每个小文件。

(4)多机+受限内存

  • 数据分发 + (3)
    • 将数据分发到多台机器上,每台机器采用(3)中的策略解决本地的数据。可采用hash+socket方法进行数据分发。
  • MapReduce
    • top K问题很适合采用MapReduce框架解决,用户只需编写一个Map函数和两个Reduce 函数,然后提交到Hadoop
      • 首先根据数据值或者把数据hash(MD5)后的值按照范围划分到不同的机器上,最好可以让数据划分后一次读入内存,这样不同的机器负责处理不同的数值范围,实际上就是Map。
      • 得到结果后,各个机器只需拿出各自出现次数最多的前N个数据,然后汇总,选出所有的数据中出现次数最多的前N个数据,这实际上就是Reduce过程。
      • 对于Map函数,采用Hash算法,将Hash值相同的数据交给同一个Reduce task;对于第一个Reduce函数,采用HashMap统计出每个词出现的频率,对于第二个Reduce 函数,统计所有Reduce task,输出数据中的top K即可。

3.爬楼梯

第一种思路

标签:数学

如果观察数学规律,可知本题是斐波那契数列,那么用斐波那契数列的公式即可解决问题,公式如下:

  • 时间复杂度:O(logn)

第一种思路代码

class Solution {
    public int climbStairs(int n) {
        double sqrt_5 = Math.sqrt(5);
        double fib_n = Math.pow((1 + sqrt_5) / 2, n + 1) - Math.pow((1 - sqrt_5) / 2,n + 1);
        return (int)(fib_n / sqrt_5);
    }
}

第二种思路

标签:动态规划

本问题其实常规解法可以分成多个子问题,爬第n阶楼梯的方法数量,等于 2 部分之和

  1. 爬上 n−1 阶楼梯的方法数量。因为再爬1阶就能到第n阶
  2. 爬上 n−2 阶楼梯的方法数量,因为再爬2阶就能到第n阶

所以我们得到公式 dp[n]=dp[n−1]+dp[n−2]
同时需要初始化 dp[0]=1 和 dp[1]=1
时间复杂度:O(n)

第二种思路代码

class Solution {
    public int climbStairs(int n) {
        int[] dp = new int[n + 1];
        dp[0] = 1;
        dp[1] = 1;
        for(int i = 2; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
}

4.合并两个有序链表

方法一:递归

思路

我们可以如下递归地定义两个链表里的 merge 操作(忽略边界情况,比如空链表等):

也就是说,两个链表头部值较小的一个节点与剩下元素的 merge 操作结果合并。

算法

我们直接将以上递归过程建模,同时需要考虑边界情况。

如果 l1 或者 l2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 l1 和 l2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null) {
            return l2;
        } else if (l2 == null) {
            return l1;
        } else if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }

    }
}

复杂度分析

  • 时间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。因为每次调用递归都会去掉 l1 或者 l2 的头节点(直到至少有一个链表为空),函数 mergeTwoList 至多只会递归调用每个节点一次。因此,时间复杂度取决于合并后的链表长度,即 O(n+m)。

  • 空间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。递归调用 mergeTwoLists 函数时需要消耗栈空间,栈空间的大小取决于递归调用的深度。结束递归调用时 mergeTwoLists 函数最多调用 n+m 次,因此空间复杂度为 O(n+m)。

方法二:迭代

思路

我们可以用迭代的方法来实现上述算法。当 l1 和 l2 都不是空链表时,判断 l1 和 l2 哪一个链表的头节点的值更小,将较小值的节点添加到结果里,当一个节点被添加到结果里之后,将对应链表中的节点向后移一位。

算法

首先,我们设定一个哨兵节点 prehead ,这可以在最后让我们比较容易地返回合并后的链表。我们维护一个 prev 指针,我们需要做的是调整它的 next 指针。然后,我们重复以下过程,直到 l1 或者 l2 指向了 null :如果 l1 当前节点的值小于等于 l2 ,我们就把 l1 当前的节点接在 prev 节点的后面同时将 l1 指针往后移一位。否则,我们对 l2 做同样的操作。不管我们将哪一个元素接在了后面,我们都需要把 prev 向后移一位。

在循环终止的时候, l1 和 l2 至多有一个是非空的。由于输入的两个链表都是有序的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。这意味着我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode prehead = new ListNode(-1);

        ListNode prev = prehead;
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                prev.next = l1;
                l1 = l1.next;
            } else {
                prev.next = l2;
                l2 = l2.next;
            }
            prev = prev.next;
        }

        // 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
        prev.next = l1 == null ? l2 : l1;

        return prehead.next;
    }
}

复杂度分析

  • 时间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。因为每次循环迭代中,l1 和 l2 只有一个元素会被放进合并链表中, 因此 while 循环的次数不会超过两个链表的长度之和。所有其他操作的时间复杂度都是常数级别的,因此总的时间复杂度为 O(n+m)。

  • 空间复杂度:O(1)。我们只需要常数的空间存放若干变量。

5.用 Rand7() 实现 Rand10()

方法一:拒绝采样

我们可以用拒绝采样的方法实现 Rand10()。在拒绝采样中,如果生成的随机数满足要求,那么久返回该随机数,否则会不断生成直到一个满足要求的随机数为止。若我们调用两次 Rand7(),那么可以生成 [1, 49] 之间的随机整数,我们只用到其中的 40 个,用来实现 Rand10(),而拒绝剩下的 9 个数,如下图所示。

我们来分析这种方法在平均情况下需要调用 Rand7() 的次数。我们称连续调用两次 Rand7() 为一轮,在第一轮中,有 40/49 的概率不被拒绝,而有 9/49 的概率被拒绝,进入第二轮。在第二轮中也是如此,因此调用 Rand7() 的期望次数为:

class Solution extends SolBase {
    public int rand10() {
        int row, col, idx;
        do {
            row = rand7();
            col = rand7();
            idx = col + (row - 1) * 7;
        } while (idx > 40);
        return 1 + (idx - 1) % 10;
    }
}

复杂度分析

  • 时间复杂度:期望时间复杂度为 O(1),但最坏情况下会达到 O(∞)(一直被拒绝)。
  • 空间复杂度:O(1)。

方法二:合理使用被拒绝的随机数

我们可以通过合理地使用被拒绝的采样,从而对方法一进行优化。

在方法一中,我们生成 [1, 49] 的随机数,若生成的随机数 x 在 [41, 49] 中,我们则拒绝 x。然而在 x 被拒绝的情况下,我们得到了一个 [1, 9] 的随机数,如果再调用一次 Rand7(),那么就可以生成 [1, 63] 的随机数。我们保留 [1, 60] 并拒绝 [61, 63]:这是 [1, 3] 的随机数。我们继续调用 Rand7(),生成 [1, 21] 的随机数,保留 [1, 20] 并拒绝 [1]。此时 [1] 已经没有任何用处,若出现了拒绝 1 的情况,我们就重新开始生成 [1, 49] 的随机数。

使用类似的期望计算方法,我们可以得到调用 Rand7 的期望次数约为 2.2123。

class Solution extends SolBase {
    public int rand10() {
        int a, b, idx;
        while (true) {
            a = rand7();
            b = rand7();
            idx = b + (a - 1) * 7;
            if (idx <= 40)
                return 1 + (idx - 1) % 10;
            a = idx - 40;
            b = rand7();
            // get uniform dist from 1 - 63
            idx = b + (a - 1) * 7;
            if (idx <= 60)
                return 1 + (idx - 1) % 10;
            a = idx - 60;
            b = rand7();
            // get uniform dist from 1 - 21
            idx = b + (a - 1) * 7;
            if (idx <= 20)
                return 1 + (idx - 1) % 10;
        }
    }
}

复杂度分析

  • 时间复杂度:期望时间复杂度为 O(1),但最坏情况下会达到 O(∞)(一直被拒绝)。
  • 空间复杂度:O(1)。

6.AVL 树和红黑树有什么区别?

红黑树:

属性:

  1. 自平衡是通过用一种两种颜色(红色或黑色)绘制每个节点来提供的。
  2. 当 Tree 被修改时,新的树随后会被重新排列和重新绘制。
  3. 它需要树中每个节点的 1 位颜色信息。

红黑树维护的约束:

  1. 根总是黑色的。
  2. 所有 NULL 叶子都是黑色的,红色节点的两个孩子都是黑色的。
  3. 从给定节点到其任何后代叶子的每条简单路径都包含相同数量的黑色节点。
  4. 从根到最远叶子的路径不超过从根到最近叶子的路径的两倍。

AVL(Adelson-Velskii 和 Landis)树

属性:

  1. 节点左右子树的高度差应小于2。
  2. 当一个节点的两个子树的高度相差超过 1 时,就会进行重新平衡。
  3. 严格平衡的更快检索。

区别:

  1. AVL 树提供比红黑树**更快的查找,**因为它们更严格平衡。
  2. 红黑树提供比 AVL 树更快的插入和移除操作,因为由于相对宽松的平衡,完成的旋转更少。
  3. AVL 树存储每个节点的平衡因子或高度,因此每个节点需要存储一个整数,而红黑树每个节点只需要 1 位信息。
  4. 红黑树用于大多数语言库,如C++ 中的map、multimap、multiset,而 AVL 树用于需要更快检索的数据库中。

7.使用递归及非递归两种方式实现快速排序

前言

本题你可以选择直接调用库函数来对序列进行排序,但意义不大。由于排序算法有很多,本文只介绍三种常见的基于比较的复杂度较低的排序。

方法一:快速排序

思路和算法

快速排序的主要思想是通过划分将待排序的序列分成前后两部分,其中前一部分的数据都比后一部分的数据要小,然后再递归调用函数对两部分的序列分别进行快速排序,以此使整个序列达到有序。

我们定义函数 randomized_quicksort(nums, l, r) 为对 nums 数组里 [l,r] 的部分进行排序,每次先调用 randomized_partition 函数对 nums 数组里 [l,r] 的部分进行划分,并返回分界值的下标 pos,然后按上述将的递归调用 randomized_quicksort(nums, l, pos - 1)和 randomized_quicksort(nums, pos + 1, r) 即可。

那么核心就是划分函数的实现了,划分函数一开始需要确定一个分界值(我们称之为主元 pivot),然后再进行划分。而主元的选取有很多种方式,这里我们采用随机的方式,对当前划分区间 [l,r] 里的数等概率随机一个作为我们的主元,再将主元放到区间末尾,进行划分。

整个划分函数 partition 主要涉及两个指针 i 和 j,一开始 i = l - 1,j = l。我们需要实时维护两个指针使得任意时候,对于任意数组下标 k,我们有如下条件成立:

我们每次移动指针 j ,如果 nums[j]>pivot,我们只需要继续移动指针 j ,即能使上述三个条件成立,否则我们需要将指针 i 加一,然后交换 nums[i] 和 nums[j],再移动指针 j 才能使得三个条件成立。

当 j 移动到 r−1 时结束循环,此时我们可以由上述三个条件知道 [l,i] 的数都小于等于主元 pivot,[i+1,r-1] 的数都大于主元 pivot,那么我们只要交换 nums[i+1] 和 nums[r] ,即能使得 [l,i+1] 区间的数都小于 [i+2,r] 区间的数,完成一次划分,且分界值下标为 i+1,返回即可。

如下的动图展示了一次划分的过程,刚开始随机选了 44 作为主元,与末尾元素交换后开始划分:

class Solution {
    public int[] sortArray(int[] nums) {
        randomizedQuicksort(nums, 0, nums.length - 1);
        return nums;
    }

    public void randomizedQuicksort(int[] nums, int l, int r) {
        if (l < r) {
            int pos = randomizedPartition(nums, l, r);
            randomizedQuicksort(nums, l, pos - 1);
            randomizedQuicksort(nums, pos + 1, r);
        }
    }

    public int randomizedPartition(int[] nums, int l, int r) {
        int i = new Random().nextInt(r - l + 1) + l; // 随机选一个作为我们的主元
        swap(nums, r, i);
        return partition(nums, l, r);
    }

    public int partition(int[] nums, int l, int r) {
        int pivot = nums[r];
        int i = l - 1;
        for (int j = l; j <= r - 1; ++j) {
            if (nums[j] <= pivot) {
                i = i + 1;
                swap(nums, i, j);
            }
        }
        swap(nums, i + 1, r);
        return i + 1;
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

复杂度分析

  • 时间复杂度:基于随机选取主元的快速排序时间复杂度为期望 O(nlogn),其中 n 为数组的长度。详细证明过程可以见《算法导论》第七章,这里不再大篇幅赘述。

  • 空间复杂度:O(h),其中 h 为快速排序递归调用的层数。我们需要额外的 O(h) 的递归调用的栈空间,由于划分的结果不同导致了快速排序递归调用的层数也会不同,最坏情况下需 O(n) 的空间,最优情况下每次都平衡,此时整个递归树高度为 logn,空间复杂度为 O(logn)。

方法二:堆排序

预备知识

  • 堆

思路和算法

堆排序的思想就是先将待排序的序列建成大根堆,使得每个父节点的元素大于等于它的子节点。此时整个序列最大值即为堆顶元素,我们将其与末尾元素交换,使末尾元素为最大值,然后再调整堆顶元素使得剩下的 n−1 个元素仍为大根堆,再重复执行以上操作我们即能得到一个有序的序列。

如下两个动图展示了对 [4, 6, 8, 5, 9] 这个数组堆排序的过程:

class Solution {
    public int[] sortArray(int[] nums) {
        heapSort(nums);
        return nums;
    }

    public void heapSort(int[] nums) {
        int len = nums.length - 1;
        buildMaxHeap(nums, len);
        for (int i = len; i >= 1; --i) {
            swap(nums, i, 0);
            len -= 1;
            maxHeapify(nums, 0, len);
        }
    }

    public void buildMaxHeap(int[] nums, int len) {
        for (int i = len / 2; i >= 0; --i) {
            maxHeapify(nums, i, len);
        }
    }

    public void maxHeapify(int[] nums, int i, int len) {
        for (; (i << 1) + 1 <= len;) {
            int lson = (i << 1) + 1;
            int rson = (i << 1) + 2;
            int large;
            if (lson <= len && nums[lson] > nums[i]) {
                large = lson;
            } else {
                large = i;
            }
            if (rson <= len && nums[rson] > nums[large]) {
                large = rson;
            }
            if (large != i) {
                swap(nums, i, large);
                i = large;
            } else {
                break;
            }
        }
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

复杂度分析

  • 时间复杂度:O(nlogn)。初始化建堆的时间复杂度为 O(n),建完堆以后需要进行 n−1 次调整,一次调整(即 maxHeapify) 的时间复杂度为 O(logn),那么 n−1 次调整即需要 O(nlogn) 的时间复杂度。因此,总时间复杂度为 O(n+nlogn)=O(nlogn)。

  • 空间复杂度:O(1)。只需要常数的空间存放若干变量。

8.如何通过一个不均匀的硬币得到公平的结果

问题描述:有一枚不均匀的硬币,已知抛出此硬币后,正面向上的概率为 p(0 < p < 1)。请利用这枚硬币产生出概率相等的两个事件。

这个问题跟之前的 利用等概率 Rand5 产生等概率 Rand3 非常像,但却简单的多。几个月前还为这个事情头疼了一下,现在想来真是不应该。

某一次抛出硬币,正面向上的概率是 p,反面向上的概率是 1 - p,当 p 不等于 0.5 时,这两个事件的概率就不一样了。怎么能凑出等概率呢?还是要利用概率的加法和乘法法则。这里用乘法,也就是连续的独立事件。

连续抛两次硬币,正反面的出现有四种情况,概率依次为:

  1. 两次均为正面:p * p
  2. 第一次正面,第二次反面:p * (1 - p)
  3. 第一次反面,第二次正面:(1 - p) * p
  4. 两次均为反面:(1 - p) * (1 - p)

这不,中间两种情况的概率是完全一样的。于是问题的解法就是连续抛两次硬币,如果两次得到的相同则重新抛两次;否则根据第一次(或第二次)的正面反面情况,就可以得到两个概率相等的事件。

用 Python 程序模拟一下这个过程,首先是一个叫做 UnbalancedCoin 的类,用来模拟这枚不均匀的硬币。Flip 方法表示抛一次硬币,返回值 True 代表正面,False 代表反面。根据要求,这个函数返回 True 和 False 的概率分别是 p 和 1 - p。函数 MakeEqualProb 利用参数 coin(这枚不均匀硬币)构造出两个事件(依旧用 True 和 False 表示),并且这两个事件的概率都是 0.5。

from random import Random

class UnbalancedCoin:
  def __init__(self, p, rand=None):
    assert 0.0 < p < 1.0, 'invalid p'
    self._p = p
    if rand is None:
      rand = Random()
    self._rand = rand

  def Flip(self):
    return self._rand.random() < self._p

def MakeEqualProb(coin):
  while True:
    a = coin.Flip()
    if a != coin.Flip():
      return a

对于不同的 p 值,模拟实验十万次,得到如下的(结果为 True 的)概率分布,其中蓝线是不均匀硬币抛出后正面向上的概率,红线是构造出来的两个事件之一(第一次正面向上,第二次反面向上)的概率。

9.给定一个包含 40 亿个无符号整数的大型文件,使用最多 1G 内存,对此文件进行排序

这两个文件分别为A和B文件,因为内存无法将所有的URL都载入,所以可以考虑如下两种方法:

1. 分而治之

  • 遍历文件A,对每个url求取hash(url)%100,然后根据所取得的值将url分别存储到100个小文件(记为a0,a1,…,a99)中。这样每个小文件的大约为10G/100 = 100M。
  • 遍历文件B,采取和A相同的方式将url分别存储到100小文件(记为b0,b1,…,b99)。这样处理后,所有可能相同的url都在对应的小文件(a0vsb0,a1vsb1,…,a99vsb99)中,不对应的小文件不可能有相同的url。然后要求出100对小文件中url的并集和相同的url。
    • 可以把其中一个小文件a(i)的url存储到hashSet中。然后遍历另一个小文件b(i)的每个url,看其是否在刚才构建的hashSet中
    • 求相同URL:如果b(i)中的url在a(i)中,那么就是共同的url,存到文件里面就可以了。
    • 合并URL:把hashSet中的url都存到文件中,如果b(i)中的url不在hashSet中,那么就存到文件里面就可以了。

2. Bloom filter - 近似算法

  • 如果允许有一定的错误率,可以使用Bloom filter
  • 1G内存大概可以表示80亿bit。将其中一个文件中的url使用Bloom filter映射为这80亿bit,然后挨个读取另外一个文件的url
  • 求相同URL:检查是否存在Bloom filter,如果是,那么该url应该是共同的url(注意会有一定的错误率)。
  • 合并文件:将第二个文件中的并集都插入到文件中

10.删除排序链表中的重复元素

方法一:一次遍历

思路与算法

由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。

具体地,我们从指针 cur 指向链表的头节点,随后开始对链表进行遍历。如果当前 cur 与 cur.next 对应的元素相同,那么我们就将 cur.next 从链表中移除;否则说明链表中已经不存在其它与 cur 对应的元素相同的节点,因此可以将 cur 指向 cur.next。

当遍历完整个链表之后,我们返回链表的头节点即可。

细节

当我们遍历到链表的最后一个节点时,cur.next 为空节点,如果不加以判断,访问 cur.next 对应的元素会产生运行错误。因此我们只需要遍历到链表的最后一个节点,而不需要遍历完整个链表。

class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if (head == null) {
            return head;
        }

        ListNode cur = head;
        while (cur.next != null) {
            if (cur.val == cur.next.val) {
                cur.next = cur.next.next;
            } else {
                cur = cur.next;
            }
        }

        return head;
    }
}

复杂度分析

  • 时间复杂度:O(n),其中 n 是链表的长度。
  • 空间复杂度:O(1)。

更多算法刷题资源分享!

请扫描👇二维码加群免费获取!记得点赞、收藏支持一下,顺便提高自己的获取速度,加群记得备注“优快云+算法”

在一堆数中取得前K个最大最小的数的方法 在一堆数中取出前K个最大最小的数的方法。这个是我们平时经常用到的排序问题,也是IT考试几乎必考的。多看看方法,还是有帮助的。 立即下载

相关推荐

日常算法实践

日常算法实践 02-06-2021:给定100G的URL磁盘数据,使用最多1G内存,统计出现频率最高的前K个URL 新建约100个文件,利用哈希(URL)%100的值,将每条URL映射到对应文件下,保证同一URL本质全部映射到同一文件下。 预先分布比较均匀,则每个文件大小大约1G,可在内存中操作,在内存中简单地统计词频即可。 对每个文件都取出相应的topK的K个<URL>根据count降序排列,形成类似一个数组,将100个文件的写入同一个文件。 对100个K对进行。得到K个即可*甚至100个K对不能都放进内存:取每个片段的一段插入内存,在K路归并步骤中,在这一段已完全被堆吐出时,再向堆中插入这一段的下一个< URL>即可。 02-07-2021:10亿个数中如何高效地找到最大的一个数以及最大的第K个数 基本同上。假设一个数为几十个字节大小,十亿个数为几十GB级,

Java求解TopK问题

TopK 问题 10亿个数中如何高效地找到最大的一个数以及最大的K个数 参考链接 参考1:https://github.com/weitingyuk/LeetCode-Notes-Waiting/blob/main/2021-02-17/TopK.md 参考2:https://zhuanlan.zhihu.com/p/72164039 方法一:全部排序 思路:将n个数进行排序后取前k个 时间复杂度:快排时间复杂度为O(nlogn)。 空间:在32位机器上,float类型占用4Byte,10910^91

m0_56706908的博客 528

利用AI辅助工具Codex在30分钟内高效生成发明专利说明书草案

Codex 是什么?简单说,它是 OpenAI 推出的一个高级 AI 工作平台,最初定位是软件开发辅助工具,但现在它的能力边界已经远远超出了写代码。根据 OpenAI 官方信息,每周有超过 500 万人使用 Codex,其中 40% 的新用户是非开发者,包括分析师、营销人员、研究人员等。这意味着 Codex 正在从一个程序员专属工具,演变为一个面向广泛知识工作者的生产力中枢。 那么,它和我们今天要谈的“写发明专利”有什么关系?核心在于 Codex 的“角色专属插件”和“上下文构建”能力。它不再只是根据零散的

weixin_29053067的博客 321

海量数据处理 - 10亿个数中找出最大的10000个数(top K问题)

[size=small] 前两天面试3面学长问我的这个问题(想说TEG的3个面试学长都是好和蔼,希望能完成最后一面,各方面原因造成我无比想去鹅场的心已经按捺不住了),这个问题还是建立最小堆比较好一些。 先拿10000个数建堆,然后一次添加剩余元素,如果大于堆顶的数(10000中最小的),将这个数替换堆顶,并调整结构使之仍然是一个最小堆,这样,遍历完后,堆中的10000个数就是所需...

飘在上海的北方人 2515

Oracle恢复学习实战小结

     学习oracle10g 043课程有段时间了,前一两个星期学习了backup/restore相关的内容,刚好公司有要求要为host服务器做一个备用机以防止数据意外丢失。于是,实战开始了。     首先介绍host服务器架构。host以oracle数据库作为底层数据存储,以公司总部开发的Santorin作为应用,主要提供给测试台架数据存储、数据访问、数据导入导出、用户权限管理和数据自动备份

kseawolfk的专栏 1036

钓鱼工具gophish史上最详细教程(附实例) —— 手把手教你成为“捕鱼人”

关于钓鱼工具 —— Gophish的超详细教程!

Ve99tr’s Blog 10万+

10亿个数中如何高效地找到最大的一个数以及最大的第 K 个数

单次遍历适用于找到最大的一个数,时间复杂度为 O(n)。堆排序适用于找到最大的第 K 个数,时间复杂度为 O(n + k log n)。快速选择算法也适用于找到最大的第 K 个数,平均时间复杂度为 O(n)。

weixin_45422672的博客 457

清华开源GPGPU“承影Ventus”初体验:用RISC-V RVV指令集跑通第一个OpenCL程序

本文详细介绍了清华大学开源GPGPU“承影Ventus”的开发实践,展示了如何基于RISC-V RVV指令集运行首个OpenCL程序。从环境配置、内核开发到性能优化,全面解析了这一创新架构的开发流程,为开发者提供了在RISC-V生态中进行通用GPU计算的实用指南。

weixin_30415113的博客 491

海量数据问题 / 巧妙算法

文章目录20亿个非负数的 int 型整数,确认指定的数 t 是否存在?Java bitSet 实现原理set 源代码:get 源码运用怎么实现 比数大 的 最小的 2 的 n 次方10亿个整数找最大的100个?如何快速判断一个数是不是 2 的 n 次方?如何快速判断一个数的奇偶性? 20亿个非负数的 int 型整数,确认指定的数 t 是否存在? 2 的 32 次方 大概是 40亿。 (1)建立...

weixin_41750142的博客 551

五步通关!宝塔Linux部署ASP.NET Core的魔法秘籍大公开!

摘要: 本文提供ASP.NET Core应用五步部署攻略,涵盖从环境配置到线上运行的完整流程: 1️⃣ 环境准备:安装宝塔面板、ASP.NET Core运行时及开放端口; 2️⃣ 应用发布:通过dotnet publish生成独立部署包并上传服务器; 3️⃣ Nginx配置:设置反向代理将域名请求转发至应用端口; 4️⃣ 进程守护:使用Supervisor确保应用持续运行; 5️⃣ HTTPS加密:通过宝塔快速申请SSL证书并解决验证路径冲突。 关键点: 强调权限设置(755)、端口开放及反向代理细节; 提

java专栏 970

T型三电平逆变器中点电位平衡详解

T型三电平逆变器(T-type Three-Level Inverter)是一种高效的多电平拓扑,相比传统NPC(Neutral Point Clamped)三电平逆变器,具有更低的开关损耗、更高效率和更简单的结构,尤其适用于中功率应用(如光伏逆变器、风电变换器和工业驱动)。(T-type Three-Level Inverter)中的应用已成为当前中高压功率变换领域(如光伏逆变器、储能PCS、电动汽车充电桩、风电并网和工业驱动)的热门方案。针对 T型逆变器,类似 NPC 的零序注入法适用。

专注于前沿技术的探索与实践,热衷于从代码逻辑、架构设计到行业应用等多维度深挖技术本质。期待在技术之路上与你共同成长~ 547

面试题:10亿int型数,统计只出现一次的数

https://blog.youkuaiyun.com/u010983881/article/details/75097358

keep12moving的博客 586

仿真系列专栏介绍

大家好!欢迎来到我的仿真学习系列专栏!这是一个我在学习仿真技术过程中整理和分享的系列文章。作为一名正在不断学习和实践的工程师,我希望通过这个专栏,和大家一起探索仿真技术,尤其是在高速信号完整性方面的应用。

案例文章,高速信号 369

金三银四冲刺力扣热门算法题!380. O(1) 时间插入、删除和获取随机元素

本题要求实现一个数据结构 RandomizedSet,它需要支持以下操作,并且所有操作的 平均 时间复杂度必须是 O(1)

引用原创内容标明出处即可 1314

基于BQ25895RTWR的锂电池充电管理硬件设计

BQ25895 是一款适用于单节锂离子电池和锂聚合物电池的高度集成型 5A 开关模式电池充电管理和系统电源路径管理器件。此类器件支持高输入电压快速充电。低阻抗电源路径对开关模式运行效率进行了优化、缩短了电池充电时间并延长了放电阶段的电池使用寿命。• 移动电源、移动 Wi-Fi 热点。• 便携式互联网器件。

corlin6688的博客 1088

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

金三银四冲刺力扣热门算法题!50. Pow(x, n)

本题要求实现 pow(x, n),即计算 x 的 n 次幂 x^n。提供python、Java、C三语答案解析

引用原创内容标明出处即可 1459

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

金三银四冲刺力扣热门算法题!134. 加油站问题

本题要求找出能够绕环路一圈的起始加油站编号,或者返回 -1 表示无法完成一圈。提供python、Java、C三语答案解析

引用原创内容标明出处即可 780

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

三面蚂蚁核心金融部,在线面试指南

第一部分必读系列: 01.学习算法和刷题的思路指南 02.学习数据结构和算法读什么书 03.动态规划解题套路框架 04.动态规划答疑篇 05.动态规划答疑篇 06.回溯算法解题套路框架 07.二分查找解题套路框架 08.滑动窗口解题套路框架 09.双指针技巧总结 10.BFS算法套路框架 11.Linux的进程、线程、文件描述符是什么 12.Git/SQL/正则表达式的在线练习平台 第二部分动态规划系列: 01.动态规划设计:最长递增子序列 02.经典动态规划:0-1 背包问题 03.经典动态规划:完

qq_38514574的博客 124

大厂Android面试核心考点与实战解析

Android系统架构作为移动开发的基石,其核心机制如Binder通信、事件分发、内存管理等原理直接影响应用性能。理解这些底层原理不仅能解决UI卡顿、内存泄漏等常见问题,更能为组件化架构设计打下基础。在工程实践中,Jetpack组件和Kotlin协程等新技术可显著提升开发效率,而启动优化、内存优化等技巧则是大厂面试的高频考点。以事件分发机制为例,通过分析ViewGroup的onInterceptTouchEvent和View的onTouchEvent源码,开发者能掌握手势冲突的解决方案。这些知识在腾讯、字节

weixin_34082854的博客 470

2026年Java面试备战全攻略:从原理到系统的进阶路线

Java面试是技术圈经久不衰的高频话题,其底层逻辑正从知识记忆型转向问题解决型。面试官不再满足于候选人背诵答案,而是通过场景追问和原理深挖,考察其独立解决复杂工程问题的能力。这一趋势要求备考者系统掌握Java基础、Spring框架、MySQL索引、Redis缓存等核心知识,并具备分布式与微服务的设计思维。技术价值的体现,在于将零散知识点织成网络,用结构化的系统设计框架应对秒杀、幂等等高频场景题。无论是校招还是社招,在金三银四前制定一份包含基础摸底、原理深挖、项目复盘、算法训练和模拟面试的完整计划,都能显著提

weixin_34239592的博客 167

春招季求职全攻略:从简历优化到薪资谈判

求职过程中的简历优化和面试技巧是每位求职者必须掌握的核心能力。在技术领域,简历的数据化表达和ATS系统关键词优化能显著提升通过率,例如使用"动词+量化结果+时间范围"的结构描述项目经验。面试环节则需掌握算法笔试的专题突破法和行为面试的STAR-L模型,这些方法能帮助求职者系统性地展示技术实力和解决问题的能力。特别是在春招季这样的求职高峰期,合理利用信息差和内推渠道,结合科学的谈薪策略,往往能事半功倍。无论是互联网大厂的技术岗还是金融行业的分析岗,这些求职技巧都具有普适性的应用价值。

weixin_30847939的博客 473

10亿条数据找出最大的1000个数

方法1:全部排序(适合大内存) 先利用快拍全部排序,在提取最大的1000个数。单机+单核+足够大内存复杂度O(nlogn)。 方法2:局部淘汰法 先排序个10000个数据maxs数组,然后之后的数据和最小的相比较,大于最小的说明之前的最小值不是最大的10000个数。此时的时间复杂度为O(n+m...

chenning2015的博客 2646

十大排序算法--基数排序(python版)

基本思想 基数排序的总体思路就是将待排序数据拆分成多个关键字进行排序,也就是说,基数排序的实质是多关键字排序 比如说成绩的排序,如果两个人总分相同,则语文高的排在前面,语文成绩也相同则数学高的排在前面 如果对数字进行排序,那么个位、十位、百位就是不同优先级的关键字 操作步骤 (1) 取得数组中的最大数,并取得位数 (2) 建立桶数组 (3) 按位数的大小分别装进不同的桶里 (4) 将原数组清空,将各个桶里的数据依次添加进原列表 (5) 再进行前一位的排序,依次循环,直到排序的位数大于最大值的位

真心乖宝宝的博客 343

经典算法应用之七----10亿数据中取最大的100个数据

给出三种思路,仅供参考。。1.思路一:根据快速排序划分的思想,每次分割之后只考虑比轴大的一部分,知道比轴大的一部分在比100多的时候,采用传统排序算法排序,取前100个。step1:递归对所有数据分成[a,b),(b,d]两个区间,(b,d]区间内的数都是大于[a,b)区间内的数step2:对(b,d]重复 step1操作,直到最右边的区间个数小于100个。注意[a,b)区间不用划分step3:返...

意中人 3567

经典排序算法总结及python实现

排序算法总结(python)1. 相关概念2. 交换排序2.1 冒泡排序(Bubble Sort)2.2 快速排序(Quick Sort)3. 插入排序3.1 简单插入排序(Insert Sort)3.2 希尔排序(Shell Sort)4. 选择排序4.1 简单选择排序(Select Sort)4.2 堆排序(Heap Sort)4.2.1 预备知识4.2.3 堆排序的基本思想及步骤5. 归并排序(Merge Sort)6. 线性时间非比较类排序6.1 计数排序(Counting Sort)6.2 桶排序

treelly的博客 1049
上一篇: Spring Cloud Kubernetes实战之配置管理(附代码实现)
下一篇: 每天一道Leecode——寻找两个正序数组的中位数,腾讯高频面试题分享!
JavaCaiy
博客等级 码龄6年 1万+粉丝 95原创
评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值