C++ : 力扣_Top(189-217)

C++ : 力扣_Top(189-217)


189、旋转数组(简单)

给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。

输入: [1,2,3,4,5,6,7] 和 k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]

输入: [-1,-100,3,99] 和 k = 2
输出: [3,99,-1,-100]
解释:
向右旋转 1 步: [99,-1,-100,3]
向右旋转 2 步: [3,99,-1,-100]
说明:

尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
要求使用空间复杂度为 O(1) 的 原地 算法。

class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        if(nums.empty()||k<0) return; // 此处不考虑k为负数的情况
        if(k>nums.size()) k = k % nums.size(); // 如果k超过了整个数组长度,则只移动余出的部分
        reverse(nums.begin(), nums.end());
        reverse(nums.begin(), nums.begin() + k);
        reverse(nums.begin() + k , nums.end());
    }
};

思路:先反转全部的数组,再按照前K个和size-K个进行分别反转即可;


190、颠倒二进制位(简单)

颠倒(逆序)给定的 32 位无符号整数的二进制位。

输入: 00000010100101000001111010011100
输出: 00111001011110000010100101000000
解释: 输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。

输入:11111111111111111111111111111101
输出:10111111111111111111111111111111
解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293,
因此返回 3221225471 其二进制表示形式为 10101111110010110010011101101001。

class Solution {
public:
    uint32_t reverseBits(uint32_t n) {
        uint32_t result = 0;
        for(int i=0; i<32; ++i){ //循环32次
            result = result << 1; // result左移
            if(n&1==1){ // 如果当前n的最后一位是1
                result = result | 1; // 对应result中的最后一位也是1
            }
            n = n >> 1; // n右移
        }
        return result;
    }
};

思路:非常简单,按照32位的每位循环,将n的低位数字赋值到result的高位数字上,每次循环将n右移,将result左移,赋值位置都是最低位;注意与或&和|的运用;


191、位1的个数(简单)

编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量)。

输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 ‘1’。

输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 ‘1’。

输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 ‘1’。

class Solution {
public:
    int hammingWeight(uint32_t n) {
        int result = 0;
        while(n>0){
            n = n & (n-1); // 将n和n-1做与运算,消除n最后面的1
            ++result; // 每消除一个1,计数+1
        }
        return result;
    }
};

思路:这道题可以进行32位循环,判断每一位是否为1;也可以巧用小技巧,没必要判断每一位,而只需要判断有1的位数就行了:n&(n-1)的结果返回的数值是将n的最后一位1变成0的结果,用这个方法可以只进行1的个数次循环;


200、岛屿数量(中等)

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。

输入:
11110
11010
11000
00000
输出: 1

输入:
11000
11000
00100
00011
输出: 3
解释: 每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成。

class Solution {
public:
    int numIslands(vector<vector<char> >& grid) {
        if(grid.empty()) return 0;
        int result = 0;
        int row = grid.size(); // 矩阵行数
        int col = grid[0].size(); // 矩阵列数
        for(int i=0; i<row; ++i){
            for(int j=0; j<col; ++j){
                if(grid[i][j]=='1'){ // 如果遇到1,则说明登录到了一座新岛
                    findBorder(grid, i, j); // 勘察该岛,并将其沉没
                    ++result; // 计数+1
                }
            }
        }
        return result;
    } // 回溯法查找这座岛
    void findBorder(vector<vector<char> >& grid, int i, int j){
        if(i<0||i>=grid.size()) return;
        if(j<0||j>=grid[0].size()) return;
        if(grid[i][j]=='0') return;
        grid[i][j] = '0';
        findBorder(grid, i+1, j);
        findBorder(grid, i-1, j);
        findBorder(grid, i, j+1);
        findBorder(grid, i, j-1);
    }
};

思路:比较简单的深度优先遍历路径搜索:遍历整个矩阵,如果遇到1,则说明这肯定是一座岛屿,然后对该坐标处开始路径搜索,将上下左右相邻的1都置为0(相当于沉没这座岛屿),然后路径搜索结束后,继续遍历矩阵,如果又遇到1,则说明这是一座新岛,再次沉没。。。


202、快乐数(简单)

编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 。

输入:19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1

class Solution {
public:
    bool isHappy(int n) {
        int slow = n, fast = n;
        while(1){
            slow = myCount(slow); // 每次慢指针推进一次变换
            if(slow==1) return true; // 如果等于了1,则是快乐数,返回true
            fast = myCount(fast); // 快指针前进两次
            fast = myCount(fast); 
            if(slow==fast) return false; // 如果快慢指针相等了,则说明出现循环,类似于链表找环
        }
    }
    int myCount(int n){
        int res = 0;
        while(n>0){
            res += pow(n%10, 2);
            n /= 10;
        }
        return res;
    }
};

思路:首先读清题意,明晰一个整数经过每位平方和运算后一定会一直减小,最后的结果只有变为1和出现无限循环两种情况;问题在于如何判断出现了循环。可以使用一个set容器,存放所有出现过的数,然后如果有数再次出现,则就是循环;这种方法是不错,但是有时循环节可能非常大,所以set占用了额外的内存空间,一种不占用空间的思路是链表找环,把数n的每一次变换状态都看作链表的一个节点,如果出现循环节,相当于这个链表有环,所以同样可以利用快慢指针的思想,slow指针每次进行一次变换,fast指针进行两次变换,如果计算过程中两者相等了,就说明出现了环;


204、计数质数(简单)

统计所有小于非负整数 n 的质数的数量。

输入: 10
输出: 4
解释: 小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。

// 常规利用6n+1、6n-1法进行判断
class Solution {
public:
    int countPrimes(int n){ // 0和1既不是质数,也不是合数
        if(n<=2) return 0; // 题目中判断范围不包括n
        if(n<=3) return 1;
        if(n<=4) return 2;
        int result = 2; // 预先存储两个质数2和3
        for(int i=5; i<n; i+=2){ // 遍历所有数(只遍历奇数即可)
            if(judge(i)) ++result; // 如果是质数,计数+1
        }
        return result;
    }
    bool judge(int n){ // 该函数只用来判断5以后的质数
        if( n%6!=1 && n%6!=5 ) return false; // 不是6的倍数两侧的数,一定不是质数
        double sqrtm = sqrt(n); // 是6的倍数两侧的数,进一步判断是不是质数
        for(int i=5; i<=sqrtm; i+=6){ // 利用(5到根号n)之间的一些除数进行判断
            if( n%i==0 || n%(i+2)==0 ){ // 如果可以被5到根号n之间的(6两侧数)整除,则说明也不是质数
                return false;
            }
        }
        return true;
    }
};
// 厄拉多塞筛法
class Solution {
public:
    int countPrimes(int n) {
        if(n<=2) return 0;
        int result = 0;
        int list[n]; // n长度的数组,初始化默认值都为0,为质数标记
        memset(list, 0, sizeof(list));
        if(n>2) ++result; // 如果大于 2 则一定拥有 2 这个质数
        for(int i=3; i<n; i+=2){  // 从 3 开始遍历,且只遍历奇数
            if(list[i]==0){  // 当前数值i的质数标记 是0,即是质数
                for(int j=3; i*j<n; j+=2){ 
                    list[i*j] = 1; // 将当前质数的奇数倍都设置成 非质数标记 1
                }
                ++result;  // 质数个数 +1
            }
        }
        return result;
    }
};

思路:首先明确0和1不是质数也不是合数;明确题目要求在小于n的数中的质数个数;除了质数2和3,其他质数质数一定符合6n+1或6n-1的公式,但满足公式的数不全是质数;可以根据这一点进行判断;首先判断n是否是6的倍数两侧的数,如果不是,肯定不是质数;如果是两侧的数,则进一步判断是否为质数:让n除以i,如果能整除,则说明不是质数(i的选取是从5到根号n之间的数,且i只需要选择这个区间内的6的倍数两侧的数即可);其中还有一点是判断每个数是不是质数的遍历中,只需要遍历奇数就好了;
该题还有很多其他筛选法,如厄拉多塞筛法,这种方法借助于长度为n的质数标记数组,从小到大进行判断和计数(也是只遍历奇数),从3开始,3为质数,所以在n范围内,3的奇数倍(除去自身)的值都不是质数,然后遍历5,把5的奇数倍都设定为非质数…这种方法的计算效率要更高一些;


206、反转链表(简单)

反转一个单链表。

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if(head==nullptr) return nullptr;
        ListNode * left = nullptr;
        ListNode * mid = head;
        ListNode * right = head;
        while(right){
            right = mid->next;
            mid->next = left;
            left = mid;
            mid = right;
        }
        return left;
    }
};

思路:基础题,设置左中右三个指针,开始的时候左指针指向头结点的前一个位置,结束的时候右节点指向尾结点的后一个位置;注意反转过程的逻辑就好;


207、课程表(中等)

你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]
给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?

输入: 2, [[1,0]]
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。

输入: 2, [[1,0],[0,1]]
输出: false
解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。

提示:

输入的先决条件是由 边缘列表 表示的图形,而不是 邻接矩阵 。详情请参见图的表示法。
你可以假定输入的先决条件中没有重复的边。
1 <= numCourses <= 10^5

class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        if(numCourses<=0 || prerequisites.empty()) return true;
        queue<int> q; // 队列,存放当前入度为0的课程节点
        vector<int> inDegree(numCourses, 0); // 统计每个节点的入度
        vector<vector<int> > list(numCourses); // 邻接表,表示每个节点为头部所指向的边
        vector<int> result; // 存放出队列的节点
        for(auto v : prerequisites){ // 遍历课程依赖数组,统计每门课在有向图节点的入度
            ++inDegree[v[1]]; // 累计入度数
            list[v[0]].push_back(v[1]); // 将对应的边添加到邻接表
        }
        for(int i=0; i<numCourses; ++i){ // 遍历入度统计数组,先将第一批入度为0的节点入队
            if(inDegree[i]==0){
                q.push(i);
            }
        }
        while(!q.empty()){
            int tmp = q.front(); // 获取当前的队列头部,一个入度为0的节点,将其对应的依赖节点入度-1
            q.pop(); // 该节点出栈
            result.push_back(tmp); // 出栈的节点由result保存
            for(auto i : list[tmp]){ // 遍历邻接表,以当前节点为头部的边的入度都-1
                if(--inDegree[i]==0){ // 对应的节点入度-1,如果为0了,则继续分离进队列
                    q.push(i);
                }
            }
        } // 循环完毕,队列为空,只要有节点的入度还不是0(没入栈出栈,result中没有此节点),说明肯定有环存在
        return result.size()==numCourses;
    }
};

思路:这道题的思路和领域属于图,这类的题不多但很有必要做一做;这道题主要问题在于解决互相依赖问题,有点类似于在资源图中搜寻产生死锁的必要条件:环。比较经典的方法是图的拓扑排序:拓扑排序指的是在一个有向无环图(DAG)中,对所有的节点进行排序,要求没有一个节点指向它前面的节点。

步骤:
先统计所有节点的入度,对于入度为0的节点就可以分离出来,然后把这个节点指向的节点的入度减一。
一直做改操作,直到所有的节点都被分离出来。
如果最后不存在入度为0的节点,那就说明有环,不存在拓扑排序,也就是很多题目的无解的情况。

具体的代码思路是建立一个数组统计节点的入度,建立一个二维数组作为图的邻接表(一维下标表示节点序号,下标指示的vector存储以该节点为头部的边的尾结点,(i,j)表示一条特定的边),利用一个队列将每次入度变为0的节点入队,直到队列为空(没有剩余入度为0的节点了),统计所有入度变为0的节点数,如果等于开始的课程数,说明没有环,即可以学完;


208、前缀树(中等)

实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。

Trie trie = new Trie();

trie.insert(“apple”);
trie.search(“apple”); // 返回 true
trie.search(“app”); // 返回 false
trie.startsWith(“app”); // 返回 true
trie.insert(“app”);
trie.search(“app”); // 返回 true

你可以假设所有的输入都是由小写字母 a-z 构成的。
保证所有输入均为非空字符串。

class Trie { // 前缀树的节点类(注意根结点处是不代表字符的)
private:
    bool isEnd; // 表示当前节点是不是一个字符串的终止节点
    Trie * next[26]; // 当前节点指向之后26个字符节点的数组
public:
    Trie() { // 前缀树节点的构造函数
        isEnd = false; 
        memset(next, 0, sizeof(next));
    }
    void insert(string word) { // 在当前节点之后插入新string
        Trie * node = this; // 用于获取当前节点的指针
        for(char c : word){
            if(node->next[c-'a']==nullptr){ // 若之后的指针没有指向新节点
                node->next[c-'a'] = new Trie(); // new一个新节点
            }
            node = node->next[c-'a']; // 节点指针指向刚new出来的节点
        }
        node->isEnd = true; // 将最后一个new出来的终止节点设定为end
    }
    bool search(string word) { // 从当前节点之后查找一个字符串(必须到头为end标识时才算查找到)
        Trie * node = this;
        for(char c : word){
            if(node->next[c-'a']==nullptr){ // 如果之后没有字符节点了,则查找失败
                return false;
            }
            node = node->next[c-'a'];
        }
        return node->isEnd; // 查找完最后一个字符,如果该节点有终止标识,则说明查找成功
    }
    bool startsWith(string prefix) { // 从当前节点之后查找有没有这个前缀(不用到头,标识不用是end)
        Trie * node = this;
        for(char c : prefix){
            if(node->next[c-'a']==nullptr){
                return false;
            }
            node = node->next[c-'a'];
        }
        return true;
    }
};

思路:很好的题,考察了高级数据结构(前缀树、Trie、字典树)的多叉树数据结构问题;首先必须明确前缀树的基本定义、性质和数据结构:1、根节点不包含字符,除根节点外每一个节点都只包含一个字符; 2、从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串; 3、每个节点的所有子节点包含的字符都不相同。明晰之后才能写出该题的代码;一个需要注意的地方是:代码中的额类Trie是前缀树的节点类,前缀树的查找、搜索前缀的方法可以从任意节点处开始,且不包括这个开始节点,要从该节点的下一个节点处开始匹配第一个字符,巧用this获取当前类的指针;


210、课程表II(中等)

现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序
可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。

输入: 2, [[1,0]]
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。

输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
说明:
输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
你可以假定输入的先决条件中没有重复的边。

提示:
这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
通过 DFS 进行拓扑排序 - 一个关于Coursera的精彩视频教程(21分钟),介绍拓扑排序的基本概念。
拓扑排序也可以通过 BFS 完成。

class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        vector<int> result;
        if(numCourses<=0) return result; 
        vector<int> inDegree(numCourses, 0); // 统计入度情况
        vector<vector<int> > edge(numCourses); // 统计边信息的二维数组
        queue<int> q; // 入度为0的节点队列q
        for(auto v : prerequisites){ // 统计入度和边信息
            edge[v[1]].push_back(v[0]);
            ++inDegree[v[0]];
        }
        for(int i=0; i<numCourses; ++i){ // 第一批入度为0的入栈
            if(inDegree[i]==0){
                q.push(i);
            }
        }
        while(!q.empty()){ // 循环处理0入度节点,将其下一节点入度-1
            int tmp = q.front();
            q.pop();
            result.push_back(tmp); // 输出最后结果
            for(auto i : edge[tmp]){ // 检索所有以这个节点为起始的边
                if(--inDegree[i]==0){
                    q.push(i); // 新的节点入度为0,入栈
                }
            }
        }
        // 检查是否所有节点最后的入度都为0,否则返回空数组
        if(result.size()!=numCourses) result.clear();
        return result;
    }
};

思路:是题目207、课程表的变形,整体思路是一模一样的,用到了图结构中的拓扑排序,只不过这道题是将拓扑排序的结果输出而已;


212、单词搜索II(困难)

给定一个二维网格 board 和一个字典中的单词列表 words,找出所有同时在二维网格和字典中出现的单词。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。

输入:
words = [“oath”,“pea”,“eat”,“rain”] and board =
[‘o’,‘a’,‘a’,‘n’],
[‘e’,‘t’,‘a’,‘e’],
[‘i’,‘h’,‘k’,‘r’],
[‘i’,‘f’,‘l’,‘v’]

输出: [“eat”,“oath”]
说明:你可以假设所有输入都由小写字母 a-z 组成。

提示:
你需要优化回溯算法以通过更大数据量的测试。你能否早点停止回溯?
如果当前单词不存在于所有单词的前缀中,则可以立即停止回溯。什么样的数据结构可以有效地执行这样的操作?散列表是否可行?为什么? 前缀树如何?

struct Node {   // 前缀树的节点(每个节点对应一个字符)
    bool word;  // 该节点处是否形成了一个单词
    string str; // 该节点形成的单词
    unordered_map<char, Node*> words; // 哈希表,存放该节点的子节点中记录的字符(节点)
};
class Trie { // 前缀树类
private:
    Node* root; // 树对象有一个根节点
public:
    Trie() { // 前缀树构造,生成根节点
        root = new Node();
    }
    void insert(string word) { // 前缀树中插入string单词
        Node* p = root; // 获取根节点
        for (char c: word) { // 遍历每个字符
            if (p->words.find(c) == p->words.end()) { // 如果该节点的子节点中没有这个字符
                Node* t = new Node(); // 新创建一个节点t存放新的字符
                p->words[c] = t; // 
            } 
            p = p->words[c]; // 往字符c的子节点移动,继续插入单词的下一个字符
        }
        p->str = word; // 插入word完毕,对应节点直接保存这个单词,方便后续使用
        p->word = true; // 对应节点为true,表示该节点处形成了一个单词
    }
    void search(vector<string>& res, vector<vector<char>>& board) { // 在前缀树中查找网格中的所有坐标点
        for (int i = 0; i < board.size(); i++) { 
            for (int j = 0; j < board[i].size(); j++) {
                help(res, board, root, i, j); // 以当前网格坐标对应的字符开始搜索单词是否存在
            }
        }
    } // 以当前网格坐标对应的字符开始搜索单词是否存在,存在则放入res结果数组
    void help(vector<string>&res, vector<vector<char>>& board, Node* p, int x, int y) {
        if (p->word) { // 如果当前节点true,表示是一个单词结束节点
            p->word = false; // 重置为false,其他方向就不会再把答案放进去了
            res.push_back(p->str); // 找到了一个单词,放入结果数组
            return;
        }  
        if (x < 0 || x == board.size() || y < 0 || y == board[x].size()) return; // 当前坐标越界
        if (p->words.find(board[x][y]) == p->words.end()) return; // 当前节点的子节点中没有对应字符,说明搜索失败
        p = p->words[board[x][y]]; // 获得当前节点的对应字符的子节点
        char cur = board[x][y]; // 当前字符cur
        board[x][y] = '#'; // 当前字符设为#号,避免后续DFS时重复进入该节点搜索
        help(res, board, p, x+1, y);
        help(res, board, p, x-1, y);
        help(res, board, p, x, y+1);
        help(res, board, p, x, y-1);
        board[x][y] = cur; // 以该坐标处为开始部分的搜索结束,重置坐标字符
    }
};
class Solution {
public:
    vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
        Trie trie; // 初始化一个前缀树trie对象,其中包括了一个根节点Node
        vector<string> res; // 结果数组
        for (string& w: words) { // 遍历字典中所有的词,将其插入前缀树对象中
            trie.insert(w);
        }
        trie.search(res, board); // 在前缀树中查找整个网格,如果遇到了前缀树中存储的单词,则输出
        return res;
        
    }
};

思路:困难的题,可以简单了解即可;这道题的主要问题在于:如果是在一个矩阵中搜索一个string的话,利用回溯路径搜索即可解决;但这道题要搜索若干个string,如果对每个string都回溯一遍,效率会很低,尤其是遇到很多前缀相同的单词,那么对矩阵中的某个点处可能要做很多次回溯路径搜索,哈希表可以试着解决这个问题,但可能比较复杂,因为哈希表很难对单词的部分前缀进行索引;好的办法是利用前缀树+回溯搜索,前缀树的结构特点可以轻松地代替原先的string数组,并且相同前缀的string都具有相同的根节点,首先利用题目给出的string数组构造一棵前缀树,然后常规使用回溯算法的时候顺便在前缀树中进行搜索判断,路径搜索每前进一步,前缀树的搜索就前进一个节点,并且这样前缀相同的string部分就不用重复回溯了,回溯只会回溯到string不同的字符的位置;不太好理解,大体了解即可;


215、数组中的第K个最大元素(中等)

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

说明:你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。

#include <algorithm> // 大小顶堆算法的头文件
#include <functional> // function容器,用于包装可调用对象
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        function<bool(int a,int b)> comp; // function容器,用于包装可调用对象
        if(k<=nums.size()/2){ // k比较小,常规建立小根堆搜索第k大即可
            comp = greater<int>(); // 利用function包装小顶堆的比较操作
        }
        else{ // k比较大,建立的堆可能会很大,故问题可以转化为寻找第(size-k+1)小的值,利用的是最大堆
            k = nums.size()-k+1; // 把k改为“第k小个数值”,并利用大顶堆
            comp = less<int>(); // 利用function包装大顶堆的比较操作
        } 
        // 后面的注释以第一种情况为准(小顶堆)
        vector<int> v(nums.begin(), nums.begin()+k); // 建立数组, 预存nums中的前k个数值
        make_heap(v.begin(), v.end(), comp); // 整理成小顶堆
        for(int i=k; i<nums.size(); ++i){ // 遍历k之后的所有元素
            if(comp(nums[i], v.front())){ // 如果当前的元素更大,则取出小顶堆的头部,把新值放入小顶堆
                pop_heap(v.begin(), v.end(), comp); // 取出小顶堆头部,放置在数组最后
                v.pop_back(); // 将这个值弹出
                v.push_back(nums[i]); // 加入新值
                push_heap(v.begin(), v.end(),comp); // 将尾部的新值一起再整理成小顶堆
            }
        }
        return v.front(); // 返回小顶堆的头部(第k小)
    }
};

思路:如果按照常规思路,先排序,在输出,复杂度是O(nlogn),如果使用小根堆解决问题,则需要遍历一次数组,每次遍历数组时在小根堆的插入删除操作复杂度是O(logk),k是小根堆的元素大小(取值1~n),故总复杂度是O(nlogk),稍稍能够减少计算复杂度,上述的代码还利用了如果k很大,则转化为求解第(size-k+1)的最小的数的问题,利用了function容器包装可调用对象;需要熟悉C++ STL中的大小根堆泛型算法;另外需要注意的是:STL中的适配器priority_queue优先级队列的底层实现其实就是利用了make_heap等泛型算法的vector,如priority_queue < int, vector, greater > q; 故该题也可以使用priority_queue来做,更加简便,但需要清楚其底层实现和计算复杂度;


217、存在重复元素(简单)

给定一个整数数组,判断是否存在重复元素。

如果任意一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。

输入: [1,2,3,1]
输出: true

输入: [1,2,3,4]
输出: false

输入: [1,1,1,3,3,4,3,2,4,2]
输出: true

class Solution {
public:
    bool containsDuplicate(vector<int>& nums) {
        unordered_set<int> list(nums.begin(), nums.end());
        return (list.size()!=nums.size());
    }
};

思路:没啥技巧的题,思路很多,先排序,再查找,或者建立哈希表,然后比较等等,从测试结果来看使用快排要比使用哈希表的方式更快一些;


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值