力扣做题记录——算法入门day1&2

Day1 二分查找

704.二分查找

  给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例1

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例2

输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

提示

 - 你可以假设 nums 中的所有元素是不重复的;
 - n 将在 [1, 10000]之间;
 - nums 的每个元素都将在 [-9999, 9999]之间。

方法 二分查找
  二分查找即数学中常用的二分法,二分查找的前提条件是有序数组,出了查找之外,二分法也可以用在排序中,因为排序本质上也是要查找合适的插入位置。
  在此题中,在一个升序数组nums中查找目标值target,对于特定下标i,比较targetnums[i]的大小。

  • 如果target>nums[i], target只能在nums[i]的右侧;
  • 如果target<nums[i], target只能在nums[i]的左侧;
  • 如果target=nums[i], i就是要寻找的下标。

  二分法的重点就是要分清target值所在的区间到底是什么!
  此外可以使用右移运算代替“/2”运算,加快代码运行速度!

代码

int search(vector<int>& nums, int target) {
        // 二分查找的必要条件是有序数组
        // 可以用位运算做除法,速度更快
        int n = nums.size();
        int left = 0, right = n - 1, mid = -1;
        while (left <= right)
        {
            mid = ((right - left) >> 1) + left;
            if (target >= nums[mid]) left = mid + 1;
            if (target <= nums[mid]) right = mid - 1;
        }
        return nums[mid] == target? mid:-1;
    }   

278.第一个错误的版本

  你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
  假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。
  你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

示例1

输入:n = 5, bad = 4
输出:4
解释:
调用 isBadVersion(3) -> false 
调用 isBadVersion(5) -> true 
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。

示例2

输入:n = 1, bad = 1
输出:1

提示

  • 1 <= bad <= n <= 231 - 1

  本题中要求尽量减少调用检查接口的次数,因此暴力解法不可取,要使用二分查找的方法。
  本题和上一题的区别主要在于区间不同,上一题下标 i 两侧的取值区间是[0, i-1]和[i+1, nums.size()]。而本题中,若isBadVersion(i)返回False,则取[0, i],若返回True,则取[i+1, nums.size()]。

代码

int firstBadVersion(int n) {
        if (n == 1) return 1;
        int mid = 0, left = 1; 
        while (left < n)
        {
            mid = ((n - left) >> 1) + left;
            if (isBadVersion(mid)) n = mid;
            else left = mid + 1;
        }
        return left; 

35.搜索插入位置

  给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
  请必须使用时间复杂度为 O(log n) 的算法。
示例1

输入: nums = [1,3,5,6], target = 5
输出: 2

示例2

输入: nums = [1,3,5,6], target = 2
输出: 1

示例3

输入: nums = [1,3,5,6], target = 7
输出: 4

示例4

输入: nums = [1,3,5,6], target = 0
输出: 0

示例5

输入: nums = [1], target = 0
输出: 0

提示

  • 1 <= nums.length <= 10^4
  • -10^4 <= nums[i] <= 10^4
  • nums 为无重复元素的升序排列数组
  • -10^4 <= target <= 10^4

  本题要求务必采用时间复杂度为O(log n)的算法,如果使用暴力查找算法,时间复杂度为O(n),二分法的时间复杂度为O(log n),因此要使用二分查找,但也将暴力算法写在下面。本题和第一题的区别主要在于target和nums[i]相等时的做法,

  • 如果target>nums[i],target所在区间为[i+1, nums.size()];
  • 如果target<=nums[i],target所在区间为[0, i]。

代码
按位查找

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

二分查找

int searchInsert(vector<int>& nums, int target) {
	int left = 0; 
	        int length = nums.size();
	        int right = length - 1;
	        int mid = 0;
	        int res = 0;
	        while (right >= left)
	        {
	            mid = ((right - left) >> 1) + left;
	            if (target <= nums[mid])
	            {
	                res = mid;
	                right = mid - 1;
	            }
	            else
	                left = mid + 1;
	        }
	        return left;
}

直接使用lower_bound(begin, end, target)函数查找
  lower_bound()函数返回指定区域内不小于target值的第一个元素的迭代器,因此在最后返回result时要用函数返回值减去nums.begin(),得到int值。

int searchInsert(vector<int>& nums, int target) {
	auto it = lower_bound(nums.begin(), nums.end(), target);
    return it - nums.begin();
}

  lower_bound()的内部实现我没有看,但有可能内部实现和二分查找相同,因为二者的运行时间和消耗内存相同。

Day2 双指针

977.有序数组的平方

  给你一个按非递减顺序排序的整数数组nums,返回每个数字的平方 组成的新数组,要求也按非递减顺序排序。
示例1

输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

示例2

输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]

提示

  • 1 <= nums.length <= 10^4
  • -10^4 <= nums[i] <= 10^4
  • nums 已按非递减顺序排序

进阶

  • 请你设计时间复杂度为O(n)的算法解决本问题

  在做题时最先想到的方法是直接平方,因为是非递减数组,因此平方后数值的大小肯定是由两端向中间递减,所以直接循环平方再排序。但后来发现,无论是否平方,平方后数值的大小顺序是不会变的,因此无需先循环平方,直接用双指针从两头开始比较就可以。因此有了以下代码:

vector<int> sortedSquares(vector<int>& nums) {
	int left = 0, right = nums.size() - 1;
	        vector<int> res;
	        // vector<int>::const_iterator it = res.begin(); 不能这样写,因为一直在往后压入数据,开头的指针一直在变
	        while (left <= right){
	            if (nums[left] * nums[left] >= nums[right] * nums[right]) 
	            {
	                res.insert(res.begin(), nums[left] * nums[left]);
	                left++;
	            }
	            else 
	            {
	                res.insert(res.begin(), nums[right] * nums[right]);
	                right--;
	            }
	        }
	        return res;
}

  这个方法的运行速度很慢,我感觉是insert()的运行速度远慢于push_back(),但如果是从大往小排,就必须从头部插入,vector没有push_front(),为了提高运行速度就要从小往大排。所以要先找到正数和负数的交界,于是有了以下代码:

vector<int> sortedSquares(vector<int>& nums) {
	int n = nums.size();
	// 先找到正数和负数的分界
        int negative = -1;
        for (int i = 0; i < n; ++i) {
            if (nums[i] < 0) {
                negative = i;
            } else {
                break;
            }
        }
	// 再从小往大排列
        vector<int> ans;
        int i = negative, j = negative + 1;
        while (i >= 0 || j < n) {
            if (i < 0) {
                ans.push_back(nums[j] * nums[j]);
                ++j;
            }
            else if (j == n) {
                ans.push_back(nums[i] * nums[i]);
                --i;
            }
            else if (nums[i] * nums[i] < nums[j] * nums[j]) {
                ans.push_back(nums[i] * nums[i]);
                --i;
            }
            else {
                ans.push_back(nums[j] * nums[j]);
                ++j;
            }
        }
        return ans;
}

  进阶方法的思路也是从大往小排,但其特殊点在于事先新建了一个固定长度的零数组,因此在插入时可以直接使用result[i]插入数据,不用insert(),速度会快一些,代码如下:

vector<int> sortedSquares(vector<int>& nums) {
	auto size = nums.size();
	        vector<int> result(size, 0);
	        int start = 0;
	        int end = size - 1;
	        for (size_t i = size - 1; end >= start && i >= 0; i--) {
	            auto endSqure = nums[end] * nums[end];
	            auto startSqure = nums[start] * nums[start];
	            if (endSqure >= startSqure) {
	                result[i] = endSqure;
	                end--;
	            } else {
	                result[i] = startSqure;
	                start++;
	            }
	        }
	        return result;
}

size_t和int
  size_t是一些C/C++标准在stddef.h中定义的。这个类型足以用来表示对象的大小。size_t的真实类型与操作系统有关。
  在32位架构中被普遍定义为:typedef unsigned int size_t;而在64位架构中被定义为:typedef unsigned long size_t;
  size_t在32位架构上是4字节,在64位架构上是8字节,在不同架构上进行编译时需要注意这个问题。而int在不同架构下都是4字节,与size_t不同;且int为带符号数,size_t为无符号数。
  为什么有时候不用int,而是用size_type或者size_t:与int固定四个字节不同有所不同,size_t的取值range是目标平台下最大可能的数组尺寸,一些平台下size_t的范围小于int的正数范围,又或者大于unsigned int. 使用Int既有可能浪费,又有可能范围不够大。
原文链接:size_t和int的区别

189.轮转数组

  给你一个数组,将数组中的元素向右轮转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 <= nums.length <= 10^5
  • -2^31 <= nums[i] <= 2^31-1
  • 0 <= k <= 10^5

进阶

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

方法1 逐个添加
  从第k位将数组分开,先将后半数组push_back入新数组,再将后半数组push_back入,再把新数组赋值给原数组。代码如下:

void rotate(vector<int>& nums, int k) {
        if (k > nums.size())
        k = k%nums.size();
        vector<int> result;
        for (int i = nums.size()-k; i < nums.size(); i++)
        result.push_back(nums[i]);
        for (int j = 0; j < nums.size()-k; j++)
        result.push_back(nums[j]);
        nums.clear();
        nums.assign(result.begin(),result.end());
}

方法2 分别反转
  从第k位将数组分开,先用reverse将整个数组反转,再分别将前后两数组反转。注意leetcode不自带reverse命令,需要自己写个函数。代码如下:

void reverse(vector<int>& nums, int start, int end) {
   while (start < end) {
        swap(nums[start], nums[end]);
        start += 1;
        end -= 1;
    }
}

void rotate(vector<int>& nums, int k) {
    k %= nums.size();
    reverse(nums, 0, nums.size() - 1);
    reverse(nums, 0, k - 1);
    reverse(nums, k, nums.size() - 1);
}

方法3 环状替换
  直接将数字逐个替换,以[1, 2, 3, 4, 5, 6, 7]k = 3为例,1换到4,4换到7,7换到3,3换到6, 6换到2,2换到5,5换到1。当nums.size()能整除k时,这样遍历可能有元素遍历不到,这种情况下,由于最终回到了起点,故该过程恰好走了整数数量的圈,不妨设为 a 圈;再设该过程总共遍历了 b 个元素。因此,我们有 an=bk,即 an 一定为 n,k的公倍数。又因为我们在第一次回到起点时就结束,因此 a 要尽可能小,故 an 就是 n,k 的最小公倍数lcm(n,k),因此 b 就为 lcm(n,k)/k
  这说明单次遍历会访问到 lcm(n,k)/k 个元素。为了访问到所有的元素,我们需要进行遍历的次数为gcd(n,k),gcd为最大公约数。代码如下:

void rotate(vector<int>& nums, int k) {
        int n = nums.size();
        k = k % n;
        int count = gcd(k, n);
        for (int start = 0; start < count; ++start) {
            int current = start;
            int prev = nums[start];
            do {
                int next = (current + k) % n;
                swap(nums[next], prev);
                current = next;
            } while (start != current);
        }
    }

  本文题目及部分答题思路来自Leetcode。

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/rotate-array/solution/xuan-zhuan-shu-zu-by-leetcode-solution-nipk/
来源:力扣(LeetCode)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值