数组/字符串
88.合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
1. 直接逃课
public class Solution {
public void Merge(int[] nums1, int m, int[] nums2, int n) {
Array.Copy(nums2, 0, nums1, m, n);
Array.Sort(nums1);
}
}
***2. 逆向双指针
从数组末尾最大值开始比较,取大值存在nums1中
public class Solution {
public void Merge(int[] nums1, int m, int[] nums2, int n) {
int point1 = m - 1;
int point2 = n - 1;
int tail = m + n - 1;
while(point1 >= 0 || point2 >= 0)
{
if(point1 < 0)
{
nums1[tail] = nums2[point2];
point2--;
}else if (point2 < 0)
{
nums1[tail] = nums1[point1];
point1--;
}else if(nums1[point1] >= nums2[point2])
{
nums1[tail] = nums1[point1];
point1--;
}else
{
nums1[tail] = nums2[point2];
point2--;
}
tail--;
}
}
}
27. 移除元素
给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。
假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:
- 更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
- 返回 k。
1. 双指针优化
一个指向首,一个指向尾。首部等于val,则赋值尾部的值,同时尾部左移;否则首部右移。
public class Solution {
public int RemoveElement(int[] nums, int val) {
int tail = nums.Length - 1;
int top = 0;
while(top <= tail)
{
if(nums[top] == val)
{
nums[top] = nums[tail--];
}else
{
top++;
}
}
return tail + 1;//记得+1,因为返回的是个数
}
}
26. 删除有序数组中的重复项
给你一个非严格递增排列的数组 nums ,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。元素的相对顺序应该保持 一致 。然后返回 nums 中唯一元素的个数。
考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
- 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
- 返回 k 。
1. 双指针
快慢指针,慢指针不等于快指针时,把快指针的值赋给慢指针。
public class Solution {
public int RemoveDuplicates(int[] nums) {
int len = nums.Length;
int left = 0;
int right = 1;
while(right < len)
{
if(nums[left] == nums[right])
{
right++;
}else
{
nums[++left] = nums[right++];
}
}
return left + 1;//记得+1,因为返回的是个数
}
}
80. 删除有序数组中的重复项 II
给你一个有序数组 nums ,请你原地删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
1. 双指针
依旧是快慢指针,慢的是排好的排好的排好的!!快的是未检测的,想成两块区域,不要混乱。
慢指针不等于快指针时,把快指针的值赋给慢指针+2的位置。有点抽象,这里我有点理解得慢。
- 其实就是不相等的时候不断把快的部分copy到慢的部分的+2。
- +2和快的部分相等,慢的和快的不相等,依旧快的部分copy到慢的+2,因为慢的部分只会保留两个相等元素,所以再两次循环就会慢的等于快的。
- 慢的等于快的时,快指针疯狂向后移,相当于忽略掉。
public class Solution {
public int RemoveDuplicates(int[] nums) {
int left = 2;
int right = 2;
int len = nums.Length;
if(len<=2){
return len;
}
while(right < len)
{
if(nums[left-2] != nums[right])
{
nums[left] = nums[right];
left++;
}
right++;
}
return left;
}
}
169. 多数元素
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
1. 排序
排序后砍半返回,因为大于1/2次数的数排序后一定在中点。
public class Solution {
public int MajorityElement(int[] nums) {
int len = nums.Length;
Array.Sort(nums);
return nums[len/2];
}
}
时间复杂度:O(nlogn)。
空间复杂度:O(logn)。
***2. Boyer-Moore 投票算法
public class Solution {
public int MajorityElement(int[] nums) {
int candidate = nums[0];
int count = 0;
foreach(int num in nums)
{
if(count == 0)
{
candidate = num;
}
count += (num == candidate) ? 1 : -1;
}
return candidate;
}
}
时间复杂度:O(n)。
空间复杂度:O(1)。
189. 轮转数组
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例 1:
输入: nums = [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]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
1. 新建一个数组
暴力解
public class Solution {
public void Rotate(int[] nums, int k) {
//方法一:额外使用一个数组
int len = nums.Length;
int[] tempArray = new int[len];
k %= len;
for(int i = 0; i < len; i++){
int temp = i+k;
if(temp >= len)
{
temp -= len;
}
tempArray[temp] = nums[i];
}
Array.Copy(tempArray, nums, len);
}
}
时间复杂度: O(n),其中 n 为数组的长度。
空间复杂度: O(n)。
2. 环状替换
- 计算数组长度和k值最大公约数,用来计算需要转几圈
- 开始转圈,每一圈内分别把当前值替换到+k的位置
public class Solution {
public void Rotate(int[] nums, int k) {
//方法二:环状替换
int len = nums.Length;
k %= len;
int circle = GCD(len, k);
for(int i = 0; i < circle; i++)
{
int cur = i;
int prev = nums[i];
do
{
int next = (cur + k) % len;
int temp = nums[next];
nums[next] = prev;
prev = temp;
cur = next;
}while(cur != i);
}
}
//GCD 算法
//欧几里得算法
//计算 x 和 y 的最大公约数。这个值用于确定需要多少个循环才能完全完成旋转操作。
//Always x > y
private int GCD(int x, int y)
{
return y == 0 ? x : GCD(y, x % y);
}
}
***3. 反转数组
反转3次数组。
例如nums[7]{1,2,3,4,5,6,7}, k = 3:
有1 2 3 4 5 6 7 -> 7 6 5 | 4 3 2 1 -> 5 6 7 | 1 2 3 4
public class Solution {
public void Rotate(int[] nums, int k) {
//方法三:反转数组
int len = nums.Length;
Array.Reverse(nums);
Array.Reverse(nums, 0, k%len);
Array.Reverse(nums, k%len, len-k%len);
}
}
其中: public static void Reverse (Array array, int index, int length);
也可以自己写Reverse()
函数。
private static void Reverse(int[] nums, int start, int end)
{
while (start < end)
{
// 交换元素
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
// 移动指针
start++;
end--;
}
}
121. 买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
1. 一次遍历
用当前值减前序最小值,只需要维护前序最小值和当前最大利润就可以了。
public class Solution {
public int MaxProfit(int[] prices) {
int res = 0;
int lowest = 100000;
for(int i = 0; i < prices.Length; i++)
{
//用两个Math函数,就不需要if判断大小了
res = Math.Max(res, prices[i] - lowest);
lowest = Math.Min(prices[i], lowest);
}
return res;
}
}
122. 买卖股票的最佳时机 II
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
***1. 贪心算法
如果赚钱那么记录,跌了就不管。
贪心算法只能用于计算最大利润,计算的过程并不是实际的交易过程。
public class Solution {
public int MaxProfit(int[] prices) {
int res = 0;
for(int i = 1; i < prices.Length; i++)
{
//也可以直接一个 res += Math.Max(0, prices[i] - prices[i-1])
int temp = prices[i] - prices[i-1];
if(temp > 0)
{
res += temp;
}
}
return res;
}
}
2. 动态规划
-
dp[i][0]
为当天手中不持有股票
计算方法:Max(前一天不持有股票&当天不买,前一天持有股票&当天卖出) -
dp[i][1]
为当天手中持有股票
计算方法:Max(前一天持有股票&当天不卖,前一天不持有股票&当天买入)
返回值为dp[len-1, 0]
,因为最后一天必定卖出。
public class Solution {
public int MaxProfit(int[] prices) {
int len = prices.Length;
int[,] dp = new int[len, 2];
dp[0, 0] = 0;
dp[0, 1] = -prices[0];
for(int i = 1; i < len; i++)
{
dp[i, 0] = Math.Max(dp[i-1, 0], dp[i-1, 1]+prices[i]);
dp[i, 1] = Math.Max(dp[i-1, 1], dp[i-1, 0]-prices[i]);
}
return dp[len-1, 0];
}
}
因为最后只返回dp[len-1, 0]
可以做一些简化:
public class Solution {
public int MaxProfit(int[] prices) {
int len = prices.Length;
int res_sold = 0;
int res_hold = -prices[0];
for(int i = 1; i < len; i++)
{
res_sold = Math.Max(res_sold, res_hold + prices[i]);
res_hold = Math.Max(res_hold, res_sold - prices[i]);
}
return res_sold;
}
}
55. 跳跃游戏
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
1. 我的第一个想法
维护一个布尔数组,后置节点可到达则为true
,依次遍历。
**但要嵌套两层循环,比较慢。并且还需要另一个数组来存储,也耗空间。
public class Solution {
public bool CanJump(int[] nums) {
int len = nums.Length;
bool[] jump = new bool[len];
jump[0] = true;
for(int i = 0; i < len; i++)
{
int temp = nums[i];
//如果当前节点可达,才进行后续处理
if(jump[i])
{
//已经超过最大长度则返回true
if(temp + i >= len - 1)
{
return true;
}
//未达最大长度时,后续可达节点均设置为true
for(int j = 1; j <= temp; j++)
{
jump[i + j] = true;
}
}
}
return false;
}
}
***2. 贪心算法
依次遍历数组中的每一个位置,并实时维护最远可以到达的位置。
public class Solution {
public bool CanJump(int[] nums) {
int len = nums.Length;
int maxJump = 0;
for(int i = 0; i < len; i++)
{
//根据前置最大步长,无法到达当前节点时,直接返回false结束循环
if(i > maxJump)
{
return false;
}
//计算最大步长
maxJump= Math.Max(maxJump, i + nums[i]);
//已经超过最大长度则返回true
if(maxJump >= len - 1)
{
return true;
}
}
return true;
}
}
45. 跳跃游戏 II
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。
每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
1. 动态规划
维护到达当前位置的最小步数
dp公式:min(到达此位置的当前最小步数,上一个位置最小步数加1跳过来)
public class Solution {
public int Jump(int[] nums) {
int len = nums.Length;
int[] dp = new int[len];
for(int i = 0; i < len; i++)
{
dp[i] = 10000;
}
dp[0] = 0;
for(int i = 0; i < len; i++)
{
for(int j = 1; j <= nums[i]; j++)
{
if(i + j < len)
{
dp[i + j] = Math.Min(dp[i] + 1, dp[i + j]);
}
}
}
return dp[len-1];
}
}
***2. 贪心算法
在循环中干两件事:
- 对比记录位于当前节点时,后续可以到达的最远节点
- 当前节点为上次跳跃后节点记录的可达最远节点时,执行跳跃,并且记录本次跳跃结束下次执行跳跃的节点
也就是说,在循环中两件事情其实是独立处理的,一边贪心找最远节点,一边执行跳跃操作。
-
i<len-1
的原因:因为在访问最后一个元素之前,记录的最远节点一定大于等于最后一个位置,否则就无法跳到最后一个位置了。如果访问最后一个元素,在边界正好为最后一个位置的情况下,我们会增加一次「不必要的跳跃次数」,因此我们不必访问最后一个元素。
public class Solution {
public int Jump(int[] nums) {
int len = nums.Length;
//跟踪当前节点可到达的最远节点
int maxJump = 0;
//上次跳跃结束达到的节点位置,后续可跳到的最远节点
int prevMax = 0;
int res = 0;
for(int i = 0; i < len-1; i++)
{
//记录当前位置时,后续可跳到的最远节点
maxJump = Math.Max(maxJump, i + nums[i]);
//执行跳跃
if(i == prevMax)
{
res++;
prevMax = maxJump;
}
}
return res;
}
}