C++ : 力扣_Top(148-179)

C++ : 力扣_Top(148-179)

文章目录

  • C++ : 力扣_Top(148-179)
      • 148、排序链表(中等)
      • 149、直线上最多的点数(困难)
      • 150、逆波兰表达式求值(中等)
      • 152、乘积最大子数组(中等)
      • 155、最小栈(简单)
      • 160、相交链表(简单)
      • 162、寻找峰值(中等)
      • 166、分数到小数(中等)
      • 169、多数元素(简单)
      • 171、Excel表列序号(简单)
      • 172、阶乘后的零(简单)
      • 179、最大数(中等)


148、排序链表(中等)

在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。

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

输入: -1->5->3->4->0
输出: -1->0->3->4->5
class Solution {
public: // 归并排序思路!先两两分割为最短链表,再两两排序合并
    ListNode* sortList(ListNode* head) {
        ListNode dummyHead(0); // 定义一个虚拟头节点(永远指向链表头结点的前一个节点)
        dummyHead.next = head; // 虚拟头节点指向链表头节点
        auto p = head;
        int length = 0;
        while(p){ // 计算链表总长度
            ++length;
            p = p->next;
        }
        for (int size = 1; size < length; size <<= 1){ // 按照不同的步长遍历链表,步长为1、2、4、8、16....
            auto cur = dummyHead.next; // 当前链表段的头节点cur
            auto tail = &dummyHead; // 上一个链表段的尾节点,用于串联每个被Merge归并排序后的链表段
            while(cur){ // 只要还有需要归并排序的链表段,则继续归并排序
                auto left = cur; // 当前的头结点left
                auto right = Spilt(left, size); // 将从当前left头节点开始的size个节点进行分离,返回之后的一个节点right
                cur = Spilt(right, size); // 将从当前right头节点开始的size个节点进行分离,返回之后的一个节点cur
                tail->next = Merge(left, right); // 对两段分离出来的链表段进行归并排序,返回头结点(前一链表段的尾结点的next)
                while(tail->next){ // 将当前链表段的头结点tail定位到当前链表段的尾节点处
                    tail = tail->next;
                }
            }
        }
        return dummyHead.next; // 返回头节点
    } // 分离链表函数Spilt,从head节点处分离一个size个节点的链表出来,返回之后的一个节点
    ListNode* Spilt(ListNode* head, int size){
        for(int i=1; i<size && head; ++i){ // 按size循环
            head = head->next;
        }
        if(!head) return nullptr; // 如果当前为链表尾部了,则返回的下一个节点就是nullptr
        ListNode* head2 = head->next; // 赋值链表段的下一个节点
        head->next = nullptr; // 分离当前链表段
        return head2; 
    } // 归并排序两个链表段
    ListNode* Merge(ListNode* n1, ListNode* n2){
        ListNode head(0); // 定义虚拟头结点,指向真正的头结点
        ListNode* node = &head;
        while(n1&&n2){ // 合并排序过程
            node->next = n1->val<n2->val ? n1 : n2;
            node = node->next;
            if(node==n1) n1 = n1->next;
            else if(node==n2) n2 = n2->next;
        }
        node->next = n1 ? n1 : n2; // 连接上剩余的链表部分
        return head.next; // 返回合并排序后的头节点
    }
};

思路:一道比较复杂的题;首先规定了严格的时间空间复杂度,O(nlogn)表明了需要使用归并排序或者快排,在链表中可以熟知的利用merge合并两个链表的函数,所以归并排序比较适合;另外空间复杂度是1,所以不能用递归,只能用循环归并代替递归的归并,具体的思路是:先对整个链表从左到右每两个节点进行merge,然后再对整个链表每4个节点(两两链表)进行merge,然后8个,16个…其中对链表进行Merge时需要使用一个Spilt函数从原始链表中分离出指定长度的链表。其中Spilt函数和Merge函数都比较好写,但循环归并的地方比较复杂,需要分离、合并后连接回去,指针的使用比较细节化;另外一个在Merge链表和主循环中涉及到链表头部节点的改变的处理函数都有一个小技巧:dummyhead,即设置一虚拟头节点,指向真正的头节点;这样可以省去不少代码量;具体的实现见代码;


149、直线上最多的点数(困难)

给定一个二维平面,平面上有 n 个点,求最多有多少个点在同一条直线上。

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

^
|
|        o
|     o
|  o  
+------------->
0  1  2  3  4

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

^
|
|  o
|     o        o
|        o
|  o        o
+------------------->
0  1  2  3  4  5  6
class Solution {
public:
    int maxPoints(vector<vector<int>>& points) {
        if(points.empty()) return 0;
        if(points.size()==1) return 1;
        int x1, y1, x2, y2, max = 0; // 点的坐标
        long double a, b; // 标记唯一的直线:y=ax+b
        map<pair<long double,long double>, int> m; // 存放所有的直线的索引及上面的点数
        map<int,int> m0; // 单独考虑斜率不存在的情况
        for(int i=0; i<points.size(); ++i){ // 遍历所有的每两个点,统计所有的直线
            x1=points[i][0]; y1=points[i][1];
            for(int j=i+1; j<points.size(); ++j){
                x2=points[j][0];  y2=points[j][1];
                if(x1==x2){ // 如果斜率不存在
                    m0.insert(make_pair(x1,0)); // 统计当前直线x=x1;
                }else{ // 如果斜率存在
                    a = (long double)(y2-y1) / (long double)(x2-x1);
                    b = (long double)(y2+y1-a*(long double)(x2+x1)) / 2;
                    pair<long double,long double> p{a,b};
                    m.insert(make_pair(p,0)); // 统计当前直线y=ax+b
                }
            }
        }
        for(int i=0; i<points.size(); ++i){ // 再次遍历所有点,看它位于哪些直线上
            x1=points[i][0]; y1=points[i][1];
            for(auto it=m.begin(); it!=m.end(); ++it){
                a = it->first.first;
                b = it->first.second;
                if(abs((a*x1+b-y1))<1e-18){ // 存在于y=ax+b的直线上(这里有精度的问题)
                    it->second += 1; // 当前直线的计数+1
                    if(max<it->second){
                        max = it->second;
                    }
                }
            }
            for(auto it=m0.begin(); it!=m0.end(); ++it){ // 看是否位于斜率不存在的直线上
                if(it->first==x1){
                    it->second += 1;
                    if(max<it->second){
                        max = it->second;
                    }
                }
            }
        }
        return max;
    }
};

思路:挺恶心的一道题;解题思路不难,穷举法即可;首先利用map统计所有的点能构成的直线y=ax,然后再次遍历所有的点,判断每个点时再遍历所有的直线,如果这个点坐标满足y=ax+b,则该直线的计数+1;该题的坑在于首先要考虑斜率不存在的情况,需要单独列一个map来保存这种直线;另外是精度的问题;关于斜率a和偏移b必须都是double或long double型;但题目中有一个测试用例连long double的精度都不够了;所以这里还涉及到高精度计算或替代计算的问题,解决方法是利用字符串模拟double除法,并把斜率用字符串表示,这样做还是比较复杂的;还有一种方法是不计算斜率,双重循环中按照两点坐标差值,然后再一重循环查找其他的点,如果对应坐标的差值交错的乘积相等,则他们就在一条直线上;上述代码没有这么做;仅供参考;总体来说,直到大致思路就可以了;另外,对于该问题,有一个用于计算机视觉图像处理的算法RANSAC可以在O(n)的复杂度内解决,但比较偏门,不多了解也罢;


150、逆波兰表达式求值(中等)

根据逆波兰表示法,求表达式的值。有效的运算符包括 +, -, *, / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

整数除法只保留整数部分。
给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

输入: ["2", "1", "+", "3", "*"]
输出: 9
解释: ((2 + 1) * 3) = 9

输入: ["4", "13", "5", "/", "+"]
输出: 6
解释: (4 + (13 / 5)) = 6

输入: ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"]
输出: 22
解释: 
  ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        if(tokens.empty()) return 0;  // 栈为空时,返回0
        int a, b, tmp;                // 当前四则运算中的变量:如tmp=a+b, 
        stack<int> stk;               // 利用栈stack挨个保存每一个计算元素
        for(string s : tokens){       // 遍历数组
            if(s=="+"||s=="-"||s=="*"||s=="/"){ // 如果当前元素是运算符
                b = stk.top(); stk.pop(); // 获取栈顶的两个元素a和b,并将其出栈
                a = stk.top(); stk.pop();
                if(s=="+") tmp = a + b;   // 按照当前的运算符计算tmp=a?b
                else if(s=="-") tmp = a - b;
                else if(s=="*") tmp = a * b;
                else if(s=="/") tmp = a / b;
                stk.push(tmp);  // 将当前的计算结果入栈
            }
            else{ // 如果当前元素是数字
                stk.push(stoi(s)); // 将数字入栈,继续遍历
            }
        }
        return stk.top(); // 遍历完毕,返回栈顶元素(此时栈中只有这一个元素)
    }
};

思路:不太难的题;利用栈挨个保存每一个计算元素,当前的计算结果也顺便放入到栈中就行了;注意不能设置额外变量保存当前结果,因为减法和除法的操作有前后顺序之分;详细过程见注释,思路很清晰;


152、乘积最大子数组(中等)

给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字)。

输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。

输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int len = nums.size();
        if(len == 0) return 0;
        if(len == 1) return nums[0];
        int result = nums[0]; 
        int maxP = nums[0]; // 初始化最大值(表示以当前元素结尾的连续数组中最大连续乘积)
        int minP = nums[0]; // 初始化最小值(表示以当前元素结尾的连续数组中最小连续乘积)
        for(int i = 1; i < n; i++) {
            int tmp = maxP;
            maxP = max( max(maxP * nums[i], nums[i]), minP * nums[i]); // 更新当前最大值
            minP = min( min( tmp * nums[i], nums[i]), minP * nums[i]); // 更新当前最小值
            result = max(maxP, result);
        }
        return result;
    }
};

思路:有点难想的题:最开始的思路很复杂:根据0进行数组分段,根据每段数组中的负数个数是偶是奇再进行判断,如果是偶,可直接元素全部相乘,如果是奇,则最大值为左边第一个负数右侧的数组元素之积、最后一个负数左侧的数组元素之积,这两者中的最大值,但程序很难写;

好的办法参考题解如下:(一次遍历,维护最大值最小值两个变量)
这题是求数组中子区间的最大乘积,对于乘法,我们需要注意,负数乘以负数,会变成正数,所以解这题的时候我们需要维护两个变量,当前的最大值,以及最小值,最小值可能为负数,但没准下一步乘以一个负数,当前的最大值就变成最小值,而最小值则变成最大值了。动态方程:
maxDP[i + 1] = max(maxDP[i] * A[i + 1], A[i + 1],minDP[i] * A[i + 1])
minDP[i + 1] = min(minDP[i] * A[i + 1], A[i + 1],maxDP[i] * A[i + 1])
dp[i + 1] = max(dp[i], maxDP[i + 1])
这里,我们还需要注意元素为0的情况,如果A[i]为0,那么maxDP和minDP都为0,
我们需要从A[i + 1]重新开始。


155、最小栈(简单)

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

push(x) —— 将元素 x 推入栈中。
pop() —— 删除栈顶的元素。
top() —— 获取栈顶元素。
getMin() —— 检索栈中的最小元素。

输入:
[“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
[[],[-2],[0],[-3],[],[],[],[]]

输出:
[null,null,null,null,-3,null,0,-2]

解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.

提示: pop、top 和 getMin 操作总是在 非空栈 上调用。

class MinStack {
    stack<int> stk1; // 保存数据
    stack<int> stk2; // 保存当前栈的最小值
public:
    MinStack() {}
    void push(int x) {
        stk1.push(x);
        if( stk2.empty() || x<stk2.top() ){ // 当最小栈为空或当前元素小于最小栈栈顶元素
            stk2.push(x); // 将当前元素入栈
        }else{ // 新元素不是最小元素
            stk2.push(stk2.top()); // 将栈顶元素再次入栈
        }
    }  
    void pop() {
        stk1.pop();
        stk2.pop();
    }
    int top() {
        return stk1.top();
    }
    int getMin() {
        return stk2.top();   
    }
};

思路:十分简单的剑指offer原题,利用两个栈分别保存数据元素和最小值元素即可;


160、相交链表(简单)

编写一个程序,找到两个单链表相交的起始节点。

注意:
如果两个链表没有交点,返回 null.
在返回结果后,两个链表仍须保持原有的结构。
可假定整个链表结构中没有循环。
程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(!headA||!headB) return nullptr;
        ListNode * tailA = headA, * tailB = headB;
        int sizeA = getListSize(&tailA); // 计算链表A的长度,tailA指向尾结点
        int sizeB = getListSize(&tailB); // 计算链表B的长度,tailB指向尾结点
        if(tailA!=tailB) return nullptr; // 如果没有交点,返回nullptr
        int diff = abs(sizeA - sizeB);
        if(sizeA>sizeB){ // 长的那个链表事先前进长度差个节点
            while(diff>0){
                headA = headA->next;
                --diff;
            }
        }else{
            while(diff>0){
                headB = headB->next;
                --diff;
            }
        }
        while(headA!=headB){ // 共同前进
            headA = headA->next;
            headB = headB->next;
        }
        return headA;
    }
    int getListSize(ListNode ** head){ // 获取链表长度
        if(!(*head)) return 0;
        int size = 1;
        while((*head)->next){
            *head = (*head)->next;
            ++size;
        }
        return size;
    }
};

思路:简单的剑指offer原题,事先计算出两个链表的长度,长的链表头节点事先前进(长度差)个节点,然后两个链表节点共同移动,如果某个节点两者相等,则就是交叉节点;


162、寻找峰值(中等)

峰值元素是指其值大于左右相邻值的元素。
给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引
数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞。

输入: nums = [1,2,3,1]
输出: 2
解释: 3 是峰值元素,你的函数应该返回其索引 2。

输入: nums = [1,2,1,3,5,6,4]
输出: 1 或 5
解释: 你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。

注意:你的解法应该是 O(logN) 时间复杂度的。
class Solution {
public:
int findPeakElement(vector& nums) {
if(nums.empty()) return -1;
int left = 0, right = nums.size()-1;
while(left int mid = (left+right)/2; // 取中间值
if(nums[mid] left = mid+1; // 注意,此处mid肯定不是峰值,所以将左边界重定位至mid+1
}
else{ // 中间值mid大于mid+1,说明mid左侧一定有峰值
right = mid; // 此时从[left,mid]区间内肯定有峰值,所以重新定位右边界
}
}
return left; // 当二分区间只有一个元素时,该节点就是峰值(建议模拟推演)
}
};

class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        if(nums.empty()) return -1;
        int left = 0, right = nums.size()-1;
        while(left<right){
            int mid = (left+right)/2; // 取中间值
            if(nums[mid]<nums[mid+1]){ // 中间值mid小于mid+1,说明mid右侧的区间内一定有峰值
                left = mid+1; // 注意,此处mid肯定不是峰值,所以将左边界重定位至mid+1
            }
            else{ // 中间值mid大于mid+1,说明mid左侧一定有峰值
                right = mid; // 此时从[left,mid]区间内肯定有峰值,所以重新定位右边界
            }
        }
        return left; // 当二分区间只有一个元素时,该节点就是峰值(建议模拟推演)
    }
};

思路:该题的困难在于必须使用复杂度O(logn)的方法去做,这示意只能采用二分的方式;于是难点在于如何判断二分划分区间的方式;题目拟定最左侧和最右侧都是负无穷,当二分区间的中间节点mid小于mid+1节点时,说明[mid+1,right]这个区间内肯定有峰值存在;当mid


166、分数到小数(中等)

给定两个整数,分别表示分数的分子 numerator 和分母 denominator,以字符串形式返回小数。
如果小数部分为循环小数,则将循环的部分括在括号内。

输入: numerator = 1, denominator = 2
输出: “0.5”

输入: numerator = 2, denominator = 1
输出: “2”

输入: numerator = 2, denominator = 3
输出: “0.(6)”

class Solution {
public:
    // 小数部分如果余数出现两次就表示该小数是循环小数了
    string fractionToDecimal(int numerator, int denominator) {
        if(denominator==0) return ""; // 边界条件,分母为0
        if(numerator==0) return "0";  // 边界条件,分子为0
        string result;
        // 转换为longlong防止溢出
        long long num = static_cast<long long>(numerator); // 分子 
        long long denom = static_cast<long long>(denominator); // 分母
        // 处理正负号,一正一负取负号
        if((num>0)^(denom>0)) result.push_back('-');
        // 分子分母全部转换为正数
        num=abs(num); denom=abs(denom); 
        //处理整数部分
        result.append(to_string(num/denom));
        //处理小数部分
        num %= denom;                       //获得余数
        if(num==0)return result;            //余数为0,表示整除了,直接返回结果
        result.push_back('.');              //余数不为0,添加小数点
        int index = result.size()-1;        //获得小数点的下标
        //map用来记录出现重复数的下标(数字,str中的下标位置),然后将'('插入到重复数前面就好了
        unordered_map<int,int> record; 
        while(num && record.count(num)==0){   //小数部分:余数不为0且余数还没有出现重复数字
            record[num] = ++index;            //记录当前出现的小数数字和它对应的位置
            num *= 10;                        //余数扩大10倍,然后求商,和草稿本上运算方法是一样的
            result += to_string(num/denom);
            num %= denom;
        }
        if(record.count(num)==1){           //出现循环余数,我们直接在重复数字前面添加'(',字符串末尾添加')'
            result.insert(record[num],"(");
            result.push_back(')');
        }
        return result;
    }
};

思路:这道题需考虑很多细节问题;整体思路不难,就是模拟正常除法的过程。难点在于:如何判断当前的除数中出现了循环?需要注意的是:如果小数中第二次出现了相同的数字,就说明循环开始了。。这一点非常重要;可以用map记录出现过的数字和对应的下标,如果第二次出现了相同的小数数字,则根据之前记录的下标位置插入括号字符即可;该题其他需要注意的地方有:正负数问题、0为分母的问题、整数范围溢出问题如(-214748364/-1)等等;


169、多数元素(简单)

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。

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

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

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int val, vote = 0;
        for(int i=0; i<nums.size(); ++i){ //遍历数组
            if(vote==0) val = nums[i];  // 如果vote计数值为0了,则将val数值变更成当前元素的值
            if(nums[i]==val) ++vote; // 如果当前元素没变,则计数+1
            else --vote; // 如果当前元素发生改变,则计数-1
        }
        return val;
    }
};

思路:剑指offer原题,可以直接排序然后取数组的中位数,但复杂度是O(nlogn);简单的方法是一种叫做摩尔投票的方法,只需一次遍历,遍历数组,如果当前两个元素相等,则vote+1,如果不等,则-1;如果vote为0,则说明之前遍历过的数组中没有那个数是最多的,将新的比较值更新为当前元素,然后继续投票;直到最后,使vote值大于零的那个元素的值,就是最多的值(众数);


171、Excel表列序号(简单)

给定一个Excel表格中的列名称,返回其相应的列序号。例如:

A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28 
...

输入: “A”
输出: 1

输入: “AB”
输出: 28

输入: “ZY”
输出: 701

class Solution {
public:
    int titleToNumber(string s) {
        if(s.empty()) return 0;
        int index = s.size() - 1;
        long int result = 0;
        for(int i=0; i<s.size(); ++i){
            int val = s[i] - 64; // 字符A对应数字65
            result += val * pow(26, index--); // 每位相加
        }
        return result;
    }
};

思路:一道简单的26进制数转十进制问题,记住ASCII码中’0’,‘A’,'a’分别对应数字:48、65、97;


172、阶乘后的零(简单)

给定一个整数 n,返回 n! 结果尾数中零的数量。

输入: 3
输出: 0
解释: 3! = 6, 尾数中没有零。

输入: 5
输出: 1
解释: 5! = 120, 尾数中有 1 个零.

说明: 你算法的时间复杂度应为 O(log n) 。

class Solution {
public:
    int trailingZeroes(int n) {
        if(n<0) return -1; // 负数没有阶乘,也没有尾后0
        long int val = 5; // 注意越界问题
        int result = 0;
        while(n>=val){
            result += n / val; // n中有几个5
            val *= 5; // 下一次循环检索n中有几个25、125、625....
        }
        return result;
    }
};

思路:该题复杂度要求必须找出规律:0的来源在于(n的阶乘)经过彻底的因式分解之后的2和5的对数;又因为2的个数一定大于5的个数,所以问题可以转换为求n的阶乘的因式分解式中的5的个数;首先计算n/5,查看n的阶乘中有多少个可以产生因数5的乘式因子,如25!中有5、10、15、20、25五个,然后再计算n/25,查看n的阶乘中有多少可以产生因数25的乘式因子,因为每个25中还多出了一个5,同理然后再n/125、n/625…把所有的商结果加和即可;


179、最大数(中等)

给定一组非负整数,重新排列它们的顺序使之组成一个最大的整数。

输入: [10,2]
输出: 210

输入: [3,30,34,5,9]
输出: 9534330
说明: 输出结果可能非常大,所以你需要返回一个字符串而不是整数。

class Solution {
public:
    string largestNumber(vector<int>& nums) {
        if(nums.empty()) return "";
        sort(nums.begin(), nums.end(), MySort); // 按指定规则排序
        string s;
        for(int n : nums){
            s.append(to_string(n));
        }
        if(s.front()=='0') return "0"; // 防止出现0打头的0000...情况
        return s;
    }
    static bool MySort(int n1, int n2){ // 自定义的比较函数(注意必须是静态函数,成员函数有隐藏参数this)
        string a = to_string(n1);
        string b = to_string(n2);
        return a+b > b+a;
    }
};

思路:最简单的做法是利用自定义的判断比较函数进行sort,注意这里sort中传入的必须是普通函数而不能是类成员函数,因为成员函数中第一个参数是隐藏参数this,可以在类中将函数其声明为静态;


你可能感兴趣的:(C++ : 力扣_Top(148-179))