Hot 100(1~10)

Hot 100(1~10)

1.两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

暴力搜索

使用两层for循环来枚举,第一层枚举每个数x,第二层从x的下标加1开始枚举,如果两数相加等于target,那么返回两数下标即可。循环结束时说明没有答案,返回空值。

vector<int> twoSum(vector<int>& nums, int target) 
{
	int n = nums.size();
	for (int i = 0; i < n; i ++)
		for (int j = i + 1; j < n; j ++)
			if (nums[i] + nums[j] == target) return {i, j};
	
	return {};
}

时间复杂度为O(N2), 没有使用额外空间,所以空间复杂度是 O(1)

哈希表

在暴力搜索中,后面每次枚举都和之前的数匹配过,所以出现了重复枚举。

使用哈希表<int, int>,第一个存储的是数组中每个的值,第二个存储每个值的下标。对于每一个x,先查询表中是否存在对应的target - x,存在直接返回,不存在则将x插入哈希表中。

vector<int> twoSum(vector<int>& nums, int target) 
{
	unordered_map<int, int> hash;
	for (int i = 0; i < nums.size(); i ++)
	{
		auto it = hash.find(target - nums[i]); // find函数返回一个迭代器,未找到则返回最后一个元素的迭代器
		if (it != hash.end()) return {it->second, i};
		hash[nums[i]] = i;
	}
	return {};
}

时间复杂度为O(N)因为只枚举了一遍数组,空间复杂度为O(N)最坏情况下存储所有数组。


2.两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

因为两个链表是逆序存储,所以第一位直接是个位,同时遍历两个链表,用一个变量carry来存储进位,逐位计算它们的和sum = n1 + n2 + carry。其中,答案链表处相应位置的数字为 sum % 10,而新的进位值为 sum / 10。当都遍历到链表末尾时,如果carry还有值,则添加到末尾,比如链表9999,另一链表已到末尾为空,最后刚好进位1。

为什么创建两个空指针,tail为了表示每个位置上的数,head为了返回答案。

ListNode* addTwoNumbers(ListNode* l1, ListNode* l2)
{
	ListNode *head = nullptr, *tail = nullptr;
	int carry = 0;
	
	while (l1 || l2)
	{
		int n1 = l1 ? l1->val : 0;
		int n2 = l2 ? l2->val : 0;
		int sum = n1 + n2 + carry;
		
		if (!head) head = tail = new ListNode(sum % 10);
		else
		{
			tail->next = new ListNode(sum % 10);
			tail = tail->next;
		}
		carry = sum / 10;
		if (carry > 0) tail->next = new ListNode(carry);
	}
	return head;
}

时间复杂度O(max(m, n)),需要遍历完最长的那个链表,空间复杂度为O(1)


3.无重复最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

滑动窗口+集合

对于字符串,如果从每个位置依次开始枚举的话,会产生重复枚举。举个例子:abcded,从a开始枚举到e是不重复的,那么从b枚举到e也是不重复的,c也是...所以,使用滑动窗口来优化掉这个问题。对于判断重复字符的问题,可以用哈希集合这种数据结构来解决。

int lengthOfLongestSubstring(string s)
{
	unordered_set<char> set;
	int n = s.size();
	
	int rk = -1, ans = 0;
	for (int i = 0; i < n; i ++)
	{
		if (i != 0) set.erase(s[i - 1]); // 左指针i往右边移动时,在集合中移除这个字符
		while (rk + 1 < n && !set.count(s[rk + 1]))
		{    				// 集合中没有右指针后一个字符,则右指针向右移动
			set.insert(s[rk + 1]); 
			rk ++;
		}
		ans = max(ans, rk - i + 1);
	}
	return ans;
}

时间复杂度是O(N),双指针会遍历一遍字符串,空间复杂度是常数级,最多需要存放不重复的子串字符


5.最长回文串

给你一个字符串 s,找到 s 中最长的回文子串。

回文串是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。

动态规划

回文串有一个性质,去掉左右两边的字符依然还是回文串。所以用f(i, j)来表示当前状态是否为回文串,状态转移方程:f(i, j) = f(i + 1, j - 1) ^ (s[i] == s[j]),只有 s[i + 1 : j - 1] 是回文串,并且字符串的第 ij 个字母相同时,s[i : j]才会是回文串。对于边界条件,长度为1是回文串,长度为二且字母相同则是回文串。

string longestPalindrome(string s)
{
	int n = s.size();
	if (n < 2) return s;
	
	int maxLen = 1, begin = 0;
	vector<vector<int>> f(n, vector<int>(n));
	for (int i = 0; i < n; i ++) f[i][i] = true; // 单个字符都是回文串,s的[1-1],[2-2],[3-3]
	
	for (int L = 2; L <= n; L ++)
	{
		for (int i = 0; i < n; i ++) // 枚举左边界
		{
			int j = L + i - 1; // 右边界
			if (j >= n) break;
			
			if (s[i] != s[j]) f[i][j] = false;
			else
			{
				if (j - i < 3) f[i][j] = true;
				else f[i][j] = f[i + 1][j - 1];
			}
			
			if (f[i][j] && j - i + 1 > maxLen)
				maxLen = j - i + 1;
				begin = i;
		}
	}
	return s.substr(begin, maxLen);
}

时间复杂度O(n2), 空间复杂度O(n2)

中心扩展算法

回文串两侧互为镜像

string longestPalindrome(string s)
{
	if (s.size() < 1) return "";
	int start = 0, end = 0;
	for (int i = 0; i < s.size(); i ++)
	{
		int len1 = expandAroundCenter(s, i, i);  // 以奇数为中心展开
		int len2 = expandAroundCenter(s, i, i + 1);  // 以偶数为中心展开
		int len = max(len1, len2);				// 选择最长的为
		if (len > end - start)
		{
			start = i - (len - 1) / 2;
			end = i + len / 2;
		}
	}
	return s.substr(start, end - start + 1);
}

int expandAroundCenter(string s, int left, int right)
{
	int l = left, r = right;
	while (l >= 0 && r < s.size() && s[l] == s[r])
		l ++;
		r --;
	return r - l - 1;
}

时间复杂度O(n2),空间复杂度O(1)


7.盛水最多的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明:你不能倾斜容器。

双指针

目的就是找出与x轴构成最大的面积。那首先高度和长度尽可能大,那么最好固定一方的最大值,长度又需要左右两条边,所以可以使用双指针算法来遍历。长度是两个指针下标之差,高度是min(height[l], height[r]),遍历时用ans更新答案,如果height[l]height[r]小,则l往右移动,去寻找是否有更长的高度,反之则往左移动。

int maxArea(vector<int>& height) 
{
	int l = 0, r = height.size() - 1, ans = 0;
	
	while (l < r)
	{
		ans = max(ans, min(height[l], height[r]) * (r - l));
		
		if (height[l] < height[r]) l ++;
		else r --;
	}
	return ans;
}

时间复杂度是O(N),双指针遍历了整个数组长度,空间复杂度是O(1)


8.三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

要找出三个元素相加等于0,可以想到用三重循环来暴力解题,但是不能有重复的元素,所以要对数组进行排序,以免出现重复枚举,并且第2个下标要大于第1个,第3个大于第2个。这还是三重循环O(N3)的做法,如果 a 和 b固定,那么 c 也是固定的,此时 a + b + c = 0,数组是排好序的,新的三元组只有可能 b‘ > b 并且 c' < c,所以可以在第一重循环的基础上,加上双指针算法。

排序+双指针

vector<vector<int>> threeSum(vector<int>& nums)
{
	vector<vector<int>> ans;
    int n = nums.size();
    sort(nums.begin(), nums.end());
    // 枚举第一个元素a
    for (int a = 0; i < n; i ++)
    {
		if (a > 0 && nums[a] == nums[a - 1]) continue; //如果和上一个数一样,则跳过
        int c = n - 1; // 第三个数,右指针
        int target = -nums[a]; // b + c = -a则是答案
        // 枚举b
        for (int b = a + 1; b < n; b ++)
        {
            if (b > a + 1 && nums[b] == nums[b - 1]) continue; //b一样的话跳过
            while (b < c && nums[b] + nums[c] > target) c --;
            
            if (b == c) break;
            if (nums[c] + nums[b] == target) ans.push_back({nums[a], nums[b], nums[c]});
        }
    }
    return ans;
}

时间复杂度O(N2),两重循环。空间复杂度O(logN)


9.电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

img

深度搜索

题目要求返回所有组合,像这种全排列问题,可以用深搜去解题。

string tmp;
vector<string> res;
vector<string> phone_map = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

vector<string> letterCombinations(string digits)
{
	if (digits.size() == 0) return res;
	dfs(0, digits);
	return res;
}

void dfs(int pos, string digits)
{
	if (pos == digits.size()) // 当所有位都被填满时,tmp存的就是答案之一
	{
		res.push_back(tmp);
		return;
	}
	
	int num = digits[pos] - '0'; // 计算出在phone_map上的索引
	for (int i = 0; i < phone_map[num].size(); i ++)
	{
		tmp.push_back(phone_map[num][i]);
		dfs(pos + 1, dights); // 递归获取填下一位
		tmp.pop_back();  // 恢复现场
	}
}

时间复杂度O(3m+4n) ,3m表示的是3个字母的个数,空间复杂度是O(m + n)


10.删除链表的倒数第N个节点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

对于链表,想要删除某个节点,必须遍历到那个节点,题目有需要返回头节点,于是可以用dummy哑节点。

两层遍历

很容易想到第一次遍历长度求出 len,然后第二次遍历到 len - n,删除节点后返回dummy

ListNode* removeNthFromEnd(ListNode* head, int n)
{
	ListNode* dummy = new ListNode(0, head);
	int len = 0;
	while (head)
	{
		len ++;
		head = head->next;
	}
	
	ListNode* cur = dummy;
	for (int i = 0; i < len - n; i ++) cur = cur->next;
	
	cur->next = cur->next->next;
	return dummy->next;
}

时间复杂度O(L),L是链表长度,会遍历L+n次,但是都是常数级且L>n,所以O(L),空间复杂度是O(1)

快慢针

可以使用双指针的来优化掉一层遍历,快指针先走 n 个节点,然后慢针从头开始和快针一起向后,当快针走到链表末尾时,此时慢针恰好在倒数第n个节点前一个节点,删掉后返回哑节点即可。

ListNode* removeNthFromEnd(ListNode* head, int n)
{
	ListNode* dummy = new ListNode(0, head);
    ListNode* first = head;
    ListNode* second = dummy;
        
    for (int i = 0; i < n; i ++) first = first->next;
        
    while (first)
    {
        first = first->next;
        second = second->next;
    }
    second->next = second->next->next;
        
    return dummy->next;
}

时间复杂度是O(L),一次遍历链表长度,空间复杂度O(1)

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