数据结构刷题:第十五天(基础)

简介: 使用哈希表存储每个数字和该数字出现的次数。遍历数组即可得到每个数字出现的次数,并更新哈希表,最后遍历哈希表,得到只出现一次的数字。

一,只出现一次的数字


136. 只出现一次的数字 - 力扣(LeetCode)

https://leetcode.cn/problems/single-number/?envType=study-plan&id=shu-ju-jie-gou-ji-chu

c46be3e729594a718ae6173532515fe0.png


1,位运算


如果不考虑时间复杂度和空间复杂度的限制,这道题有很多种解法,可能的解法有如下几种。


使用集合存储数字。遍历数组中的每个数字,如果集合中没有该数字,则将该数字加入集合,如果集合中已经有该数字,则将该数字从集合中删除,最后剩下的数字就是只出现一次的数字。


使用哈希表存储每个数字和该数字出现的次数。遍历数组即可得到每个数字出现的次数,并更新哈希表,最后遍历哈希表,得到只出现一次的数字。


使用集合存储数组中出现的所有数字,并计算数组中的元素之和。由于集合保证元素无重复,因此计算集合中的所有元素之和的两倍,即为每个元素出现两次的情况下的元素之和。由于数组中只有一个元素出现一次,其余元素都出现两次,因此用集合中的元素之和的两倍减去数组中的元素之和,剩下的数就是数组中只出现一次的数字。


上述三种解法都需要额外使用 O(n) 的空间,其中 n 是数组长度。


如何才能做到线性时间复杂度和常数空间复杂度呢?


答案是使用位运算。对于这道题,可使用异或运算 ⊕。异或运算有以下三个性质。


任何数和 0 做异或运算,结果仍然是原来的数,即a⊕0=a。

任何数和其自身做异或运算,结果是 0,即 a⊕a=0。

异或运算满足交换律和结合律,即 a⊕b⊕a=b⊕a⊕a=b⊕(a⊕a)=b⊕0=b。


bda106fa80584111ac561d5e84c96a2b.png


class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int ret = 0;
        for (auto e: nums) ret ^= e;
        return ret;
    }
};


复杂度分析


  • 时间复杂度:O(n),其中 nn 是数组长度。只需要对数组遍历一次。


  • 空间复杂度:O(1)。


二,多数元素


169. 多数元素 - 力扣(LeetCode)

https://leetcode.cn/problems/majority-element/?plan=data-structures&plan_progress=zz5yyb3


f6d764dabfb04358867dc0ae5516cec6.png


说明

最简单的暴力方法是,枚举数组中的每个元素,再遍历一遍数组统计其出现次数。该方法的时间复杂度是 O(n^2),会超出时间限制,因此我们需要找出时间复杂度小于 O(n^2)的优秀做法。


1,哈希表


思路


我们知道出现次数最多的元素大于 n/2 次,所以可以用哈希表来快速统计每个元素出现的次数。


算法


我们使用哈希映射(HashMap)来存储每个元素以及出现的次数。对于哈希映射中的每个键值对,键表示一个元素,值表示该元素出现的次数。


我们用一个循环遍历数组 nums 并将数组中的每个元素加入哈希映射中。在这之后,我们遍历哈希映射中的所有键值对,返回值最大的键。我们同样也可以在遍历数组 nums 时候使用打擂台的方法,维护最大的值,这样省去了最后对哈希映射的遍历。


class Solution {
public:
    int majorityElement(vector<int>& nums) {
        unordered_map<int, int> counts;
        int majority = 0, cnt = 0;
        for (int num: nums) {
            ++counts[num];
            if (counts[num] > cnt) {
                majority = num;
                cnt = counts[num];
            }
        }
        return majority;
    }
};


复杂度分析


时间复杂度:O(n),其中 n 是数组 nums 的长度。我们遍历数组 nums 一次,对于 nums 中的每一个元素,将其插入哈希表都只需要常数时间。如果在遍历时没有维护最大值,在遍历结束后还需要对哈希表进行遍历,因为哈希表中占用的空间为 O(n)(可参考下文的空间复杂度分析),那么遍历的时间不会超过 O(n)。因此总时间复杂度为 O(n)。


空间复杂度:O(n)。


343a798b10904c78aac55897533c240c.png


2,排序


思路


如果将数组 nums 中的所有元素按照单调递增或单调递减的顺序排序,那么下标为n/2的元素(下标从 0 开始)一定是众数。


算法


对于这种算法,我们先将 nums 数组排序,然后返回上文所说的下标对应的元素。下面的图中解释了为什么这种策略是有效的。在下图中,第一个例子是 n 为奇数的情况,第二个例子是 n 为偶数的情况。


3ad7d23fb808226b59e254c8468dbd87.png

7a76074903c849b09a3f9954e0ca6205.png


class Solution {
public:
    int majorityElement(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        return nums[nums.size() / 2];
    }
};


复杂度分析


时间复杂度:O(nlogn)。将数组排序的时间复杂度为 O(nlogn)。


空间复杂度:O(logn)。如果使用语言自带的排序算法,需要使用 O(logn) 的栈空间。如果自己编写堆排序,则只需要使用 O(1) 的额外空间。


3,随机化


思路


因为超过 n/2的数组下标被众数占据了,这样我们随机挑选一个下标对应的元素并验证,有很大的概率能找到众数。


算法


由于一个给定的下标对应的数字很有可能是众数,我们随机挑选一个下标,检查它是否是众数,如果是就返回,否则继续随机挑选。


class Solution {
public:
    int majorityElement(vector<int>& nums) {
        while (true) {
            int candidate = nums[rand() % nums.size()];
            int count = 0;
            for (int num : nums)
                if (num == candidate)
                    ++count;
            if (count > nums.size() / 2)
                return candidate;
        }
        return -1;
    }
};


看题解:


多数元素 - 多数元素 - 力扣(LeetCode)

https://leetcode.cn/problems/majority-element/solution/duo-shu-yuan-su-by-leetcode-solution/

三,三数之和


15. 三数之和 - 力扣(LeetCode)

https://leetcode.cn/problems/3sum/?plan=data-structures&plan_progress=zz5yyb3


6a17511e6d6746ff8d6c857c00f88ed6.png


看题解:


三数之和 - 三数之和 - 力扣(LeetCode)

https://leetcode.cn/problems/3sum/solution/san-shu-zhi-he-by-leetcode-solution/

目录
相关文章
|
7月前
【数据结构刷题】消失的数字和轮转数组(上)
【数据结构刷题】消失的数字和轮转数组(上)
|
1天前
数据结构--链表刷题(一)快慢指针(上)
数据结构--链表刷题(一)快慢指针
16 0
|
1天前
|
机器学习/深度学习
数据结构:力扣刷题1
数据结构:力扣刷题1
17 0
|
7月前
|
算法 搜索推荐
字节跳动大神手写长达1134页的数据结构与算法刷题指南,简直绝了
前言 为什么要学习数据结构与算法呢?归根结底,你学习一个东西是因为你觉得他有收益,那么学习数据结构与算法,收益在哪里呢? 短期收益是应对考试、面试。 长期收益是“用”,来解决实际工程问题。 如果你在一家成熟的公司,或者 BAT 这样的大公司,面对的是千万级甚至亿级的用户,开发的是 TB、PB 级别数据的处理系统。性能几乎是开发过程中时刻都要考虑的问题。一个简单的 ArrayList、Linked List 选择问题,就可能会产生成千上万倍的性能差别。这个时候,数据结构和算法的意义就完全凸显出来了。
52 0
|
11月前
牛客网《剑指offer》专栏刷题练习|锻炼递归思想|练习栈的使用
牛客网《剑指offer》专栏刷题练习|锻炼递归思想|练习栈的使用
68 0
|
12月前
|
存储 Go C语言
go语言|数据结构:单链表(3)刷题实战
go语言|数据结构:单链表(3)刷题实战
105 0
《Java数据结构基础》“队列的实现”和“刷题实战演练”
《Java数据结构基础》“队列的实现”和“刷题实战演练”
|
机器学习/深度学习 算法
数据结构刷题:第十三天
时间复杂度:O(N),其中 N 是二叉搜索树的节点数。最坏情况下二叉搜索树是一条链,且要找的元素比链末尾的元素值还要小(大),这种情况下我们需要递归 N 次。
43 0
数据结构刷题:第十三天
|
存储 算法
数据结构刷题:第十二天
这是一道很经典的二叉树问题。显然,我们从根节点开始,递归地对树进行遍历,并从叶子节点先开始翻转。如果当前遍历到的节点 root 的左右两棵子树都已经翻转,那么我们只需要交换两棵子树的位置,即可完成以 root 为根节点的整棵子树的翻转。
53 0
数据结构刷题:第十二天
|
机器学习/深度学习 算法
数据结构刷题:第十天
时间复杂度:O(n),其中 nn 为二叉搜索树的节点个数。Morris 遍历中每个节点会被访问两次,因此总时间复杂度为 O(2n)=O(n)。
33 0
数据结构刷题:第十天