Hot 100(11~20)

Hot 100(11~20)

11.有效的括号

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。

辅助栈

对于判断这种左右匹配,可以考虑用栈和哈希表来辅助。

思路就是遍历字符串中的每个字符,如果遍历到哈希表中的key(也就是右括号),并且如果此时的栈为空(栈中没有与之对应的左括号)或者栈顶不是对应的左括号,则是不匹配的返回false,匹配的话就弹出栈顶。遍历到左括号直接压入栈即可。注意特殊情况,匹配肯定是成对匹配,如果字符串长度不是偶数,则直接返回。

bool isValid(string s)
{
    if (string.size() % 2 != 0) return false;
    unordered_map<char, char> hash = {
        {')', '('},
        {']', '['},
        {'}', '{'}
    };
    stack<char> stk;
    
    for (auto ch : s)
    {
        if (hash.count(ch)) // count函数直接返回的是一个数值,如果存在那么返回1,反之0
        {
            if (stk.empty() || stk.top() != hash[ch]) return false;
            stk.pop();
        }
        else stk.push(ch);
    }
    return stk.empty();
}

时间复杂度O(n),n是字符串长度,空间复杂度是O(n+6),用栈保存字符串空间复杂度为n,6则是哈希表


12.合并两个链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

合并为一个新的链表,使用一个指针逐个遍历比较,然后修改next。要新建一个头节点,方便返回。

ListNode* mergeTwoLists(ListNode* l1, ListNode* l2)
{
    ListNode* head = new ListNode(-1);
    ListNode* pre = head;
    
    while (l1 != nullptr && l2 != nullptr)
    {
        if (l1->val > l2->val) 
        {
            pre->next = l2;
            l2 = l2->next;
        }
        else
        {
            pre->next = l1;
            l1 = l1->next;
        }
    }
    pre->next = l1 == nullptr ? l2 : l1;
    return head->next;
}

时间复杂度O(n+m),n和m是两个链表的长度, 空间复杂度O(1)


13.括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

看到这个类似全排列的问题,可以用暴力解题,题解思路是列出所有可能的组合,然后再判断它是否有效。但是这种时间复杂度过高,一般不会考虑。大多数会使用深搜dfs来解题,第一位放什么,然后往下递归,构建二叉树。

dfs递归

关键点在于搜索序列时的当前位,是选择左括号还是右括号,并且需要生成有效的括号序列。

首先左括号数量不能大于n,当条件成立时添加左括号。其次当右边括号小于n并且左边括号大于右括号时,添加右括号。递归的结束条件是左右两遍括号数量都等于n。

参数传递是值传递,值传递的好处就是下一层的结果不会对上一层造成任何影响,因此我们回溯到上一层时,程序会自动帮我们擦除当前层的选择。

public:
    vector<string> res; //记录答案 
    vector<string> generateParenthesis(int n) {
        dfs(n , 0 , 0, "");
        return res;
    }
    void dfs(int n ,int lc, int rc ,string str)
    {
        if( lc == n && rc == n) res.push_back(str);    //递归边界
        else
        {
            if(lc < n) dfs(n, lc + 1, rc, str + "(");            //拼接左括号
            if(rc < n && lc > rc) dfs(n, lc, rc + 1, str + ")"); //拼接右括号
        }
    }

时间复杂度是$O(C_{2n}^n)$。空间复杂度$O(n)$,递归2n层,每层常数空间使用,所以渐进复杂为O(n)


15.下一个排列

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。
给你一个整数数组 nums ,找出 nums 的下一个排列。

必须 原地 修改,只允许使用额外常数空间。

这题的题意是按照字典序找出下一个排列。下一个排列比当前排列要大,并且变大的幅度需要最小。如果更改前面的数字,那么变大的幅度是最大的,所以进行修改后面的数字。

两次扫描

第一次从后往前寻找第一个逆序。第二次扫描找出比第一个它大的位。

void nextPermutation(vector<int>& nums)
{
	int i = nums.size() - 2; // 为了找出后面的第一个逆序
	while (i >= 0 && nums[i] >= nums[i + 1]) i --; // 寻找逆序
    
    if (i >= 0) 
    {
        int j = nums.size() - 1; // 寻找第一个比i大的数字 
        while (nums[i] >= nums[j]) j --;
        swap(nums[i], nums[j]);
    }
    reverse(nums.begin() + i + 1, nums.end()); // 将i后面的数反转
}

时间复杂度是$O(N)$,N是给定序列长度,最多需要两次扫描,以及一次反转。空间复杂度$O(1)$


17.搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

二分查找

最简单的办法就是开一个哈希表记住值和下标,sort排序看看 nums[target] 是否等于 target ,如果不是则返回-1,是则从哈希表中返回value。但是本题需要用$O(logn)$的方法求解,于是可以用二分的方法。将旋转后的数组一分为二,一部分是有序的,另一部分是部分有序的,

int search(vector<int>& nums, int target)
{
    int n = nums.size();
    if (!n) return -1; // 特判数组为空或者只有一个数的情况
    if (n == 1) return nums[0] == target ? 0 : -1;
    
    int l = 0, r = n - 1;
    while (l <= r)
    {
        int mid = l + r >> 1;
        if (nums[mid] == target) return mid;
        
        if (nums[0] <= nums[mid]) // 说明这部分有序
        {				// 如果target在0到mid这个区间,更新右边界
            if (nums[0] <= target && target < nums[mid]) r = mid - 1;
            else l = mid + 1;
        }
        else 
        {
            if (nums[mid] < target && target <= nums[n - 1]) l = mid + 1;
            else r = mid - 1;
        }
    }
    return -1; // 退出while循环说明没有该target
}

时间复杂度$O(log n)$,空间复杂度$O(1)$


18.在排序数组中查找元素的第一个和最后一个位置

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

进阶:

你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?

最容易让人想到的解题方式就是直接遍历,如果没搜到target,直接返回{-1,-1},如果搜到了,则记下当前下标,直到遍历比target大的数,返回下标组。但这种解题方式是O(n)的。

这题类似于ACWing上的数的范围,因为给出的数组是有序的,所以可以使用二分查找来优化时间复杂度。

二分查找

vector<int> searchRange(vector<int>& nums, int target)
{
    int n = nums.size();
    if (!n) return {-1, -1};
	int l = 0, r = n - 1;
    int left = 0;
    while (l < r)
    {
        int mid = l + r >> 1;
        if (nums[mid] >= target) r = mid;
        else l = mid + 1;
    }
    
    if (nums[l] != target) return {-1, -1};
    else 
    {
        left = l;
        r = n - 1;
        while (l < r)
        {
            int mid = l + r >> 1;
            if (nums[mid] <= target) l = mid;
            else r = mid - 1;
        }
        return {left, r};
    }
    return {-1, -1};
}

时间复杂度为$O(logn)$,空间复杂度为$O(1)$


19.组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

对于这类寻找可行解的问题,都可以用搜索回溯的方法。从数组第0位开始,每次选择该位或不选择该位,向下搜索可形成一棵树。

递归函数dfs需要维护三个参数,一是目标值target,每次选择一位后target减去该位。二是当前的组合combine。三是下标dix,如果选择当前位,则combine + 1,idx不变(因为可以有重复),如果不选择该位,则idx + 1。

递归结束条件:下标等于candidates的size;

vector<vector<int>> combinationSum(vector<int>& candidates, int target)
{
	vector<vector<int>> ans; // combine里加起来为target则添加到ans里
    vector<int> combine; // 当前组合
    dfs(candidates, target, ans, combine, 0); // 下标从0开始
    return ans;
}
		 // 给定的数组               目标和              答案数组               当前组合              下标
void dfs(vector<int>& candidates, int target, vector<vector<int>>& ans, vector<int>& combine, int idx)
{
    if (idx == candidates.size()) return;
    if (target == 0) 
    {
        ans.push_back(combine);
        return;
    }
    
    // 跳过当前位
    dfs(candidates, target, ans, combine, idx + 1);
    
    // 选择当前位
    if (target - candidates[idx] >= 0)
    {
        combine.push_back(candidates[idx]);
        dfs(candidates, target - candidates[idx], ans, combine, idx);
        combine.pop_back(); // 恢复现场
    }
}

时间复杂度$O(S)$,取决于搜索树所有叶子节点的深度之和。空间复杂度$O(target)$最坏递归target层


posted @ 2022-05-07 15:37  FailBetter  阅读(38)  评论(0编辑  收藏  举报