C++ 数据结构与算法(一)(复杂度、数组、二分查找)

时间复杂度

时间复杂度是一个函数,它定性描述该算法的运行时间。

假设算法的问题规模为n,算法的渐近时间复杂度,简称时间复杂度,记为 O ( f ( n ) ) O(f(n)) O(f(n))

大O用来表示上界的,当用它作为算法的最坏情况运行时间的上界,就是对任意数据输入的运行时间的上界。
但是业内的一个默认规定,O代表的就是一般情况,而不是严格的上界。
深入探讨一个算法的实现以及性能的时候,就要时刻想着数据用例的不一样,时间复杂度也是不同的

  • O(1)常数阶 < O ( log ⁡ n ) O(\log n) O(logn)对数阶 < O ( n ) O(n) O(n)线性阶 < O ( n log ⁡ n ) O(n\log n) O(nlogn) < O ( n 2 ) O(n^2) O(n2)平方阶 < O ( n 3 ) O(n^3) O(n3)立方阶 < O ( 2 n ) O(2^n) O(2n)指数阶 < O ( n ! ) O(n!) O(n!) < O ( n n ) O(n^n) O(nn)
  • log n忽略底数的描述

空间复杂度

空间复杂度预估程序运行时占用内存的大小,而不是可执行文件的大小。

数组

数组是存放在连续内存空间上的相同类型数据的集合。

二分查找

二分法总结:
https://leetcode-cn.com/problems/search-insert-position/solution/te-bie-hao-yong-de-er-fen-cha-fa-fa-mo-ban-python-/

升序数组 nums 中寻找目标值 target,对于特定下标 i,比较nums[ i ] 和 target 的大小:

  • 如果 n u m s [ i ] = t a r g e t nums[i] = target nums[i]=target,则下标 i 即为要寻找的下标;
  • 如果 n u m s [ i ] > t a r g e t nums[i] > target nums[i]>target,则 target只可能在下标i的左侧;
  • 如果 n u m s [ i ] < t a r g e t nums[i] < target nums[i]<target,则 target只可能在下标i的右侧。

基于上述事实,可以在有序数组中使用二分查找寻找目标值,每次查找都会将查找范围缩小一半。

  • 时间复杂度 O ( log ⁡ n ) O(\log n) O(logn),其中 n 是数组的长度
  • 空间复杂度 O ( 1 ) O(1) O(1)

每次循环中 leftright 共同约束了本次查找的范围, 要让本次循环与上一次循环查找的范围既不重复(重复了会引起死循环), 也不遗漏, 并且要让 leftright 共同约束的查找的范围变得无意义时不再进行查找(即跳出 while)(否则会导致访问越界), 这其实就是所谓的循环不变量。因此要清楚对查找区间的定义,在循环中根据查找区间的定义来做边界处理
在这里插入图片描述
定义查找区间为 [left, right][left, right),LeetCode示例:

704. 二分查找●

class Solution {
public:
    int search(vector<int>& nums, int target) {
       	// 1、定义查找区间为 [left, right]
        int left = 0;
        int right = nums.size() - 1; 	// [left, right]
        while (left <= right){
            int middle = left + (right - left) / 2; //防溢出
            if (nums[middle] > target){
                right = middle - 1;		// [left, right]
            }
            else if (nums[middle] < target){
                left = middle + 1;		// [left, right]
            }
            else{
                return middle;
            }
        }
        return -1;

        // 2、定义查找区间为 [left, right)
        int left = 0;
        int right = nums.size();			// [left, right)
        while (left < right){
            int middle = left + (right - left) / 2;
            if (nums[middle] > target){
                right = middle;				// [left, right)
            }
            else if (nums[middle] < target){
                left = middle + 1;			// [left, right)
            }
            else{
                return middle;
            }
        }
        return -1; // 查找失败
    }
};

35. 搜索插入位置●

class Solution {
public:
    // 无重复元素
    int searchInsert(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        while (left <= right){
            int middle = left + (right - left) / 2;
            if (nums[middle] > target){
                right = middle - 1;
            }
            else if (nums[middle] < target){
                left = middle + 1;
            }
            else{ // nums[middle] == target
                return middle;    // 无重复元素
            }
        }
        return left; // = right + 1
    }
};

循环判断中,等于target的情况不跳出循环,继续移动搜索指针,最终将会导致 left = right + 1 的情况而跳出循环。

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        while (left <= right){
            int middle = left + (right - left) / 2;
            if (nums[middle] >= target){	// 大于或等于 即 左移right
                right = middle - 1;			// 最后一个小于 num 的位置索引
            }
            else{ // nums[middle] < target	// 小于	即 右移left
                left = middle + 1;			// 第一个大于或等于 num 的位置索引
            }
        }
        // 有重复元素
        // 寻找第一个大于或等于 num 的位置索引,返回left
        return left; // = right + 1
        // 寻找最后一个小于 num 的位置索引,返回right
        // return right;  // = left + 1
    }
};
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        while (left <= right){
            int middle = left + (right - left) / 2;
            if (nums[middle] > target){		// 大于 即 左移 right
                right = middle - 1;			// 最后一个小于或等于 num 的位置索引
            }
            else{ // nums[middle] <= target	// 小于或等于 即 右移left
                left = middle + 1;			// 第一个大于 num 的位置索引
            }
        }
        // 有重复元素
        // 寻找第一个大于 num 的位置索引,返回left
        return left; // = right + 1
        // 寻找最后一个小于或等于 num 的位置索引,返回right
        // return right;  // = left + 1
    }
};

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

1、遍历

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {    
    // 1、遍历
        int n = nums.size();
        int left = -1;	//左边界
        int right = -1; //右边界
        for ( int i = 0; i < n; ++i){  // 从左往右遍历
            if (nums[i] == target){
               if (left == -1){
                   left = i;
                   right = i;
               }
               else{
                   right = i;
               }
           }    
        }
        return vector<int>{left,right};
    }
};

2、 二分法

class Solution {
public:
    //   寻找第一个大于或等于 num 的位置索引, 找不到则返回数组长度 (相当于 35. 搜索插入位置)
    int search (vector<int>& nums, int num) {    
        int left = 0;
        int right = nums.size() - 1;
        while (left <= right){
            int mid = left + (right - left) / 2;
            if (nums[mid] >= target){		// 大于或等于 即 左移right
                right = mid - 1;			// 最后一个小于 num 的位置索引
            }
            else{ // nums[mid] < target	// 小于	即 右移left
                left = mid + 1;			// 第一个大于或等于 num 的位置索引
            }
        }
        return left;    // 寻找第一个大于或等于 num 的位置索引,返回left
                        // 寻找最后一个小于 num 的位置索引,返回right
    }

    // 四种情况:
    //  1、target 小于 nums 中最小 : left = right = 0
    //  2、target 大于 nums 中最大 : left = right = nums.size()
    //  3、target 在 nums 大小范围内,但不在其中:left = right = search(nums, target + 1)
    //  4、target 在 nums 数组中:left < right
    vector<int> searchRange(vector<int>& nums, int target) {
        int left = search(nums, target);        //   寻找第一个大于或等于 target 的位置索引
        int right = search(nums, target + 1);   //   寻找第一个大于或等于 target+1 的位置索引
        if (left < right){
            return vector<int> {left, right-1};
        }
        return vector<int> {-1, -1};
    }
};

69.x的平方根 ●

找到满足 k 2 ≤ x k^2 ≤ x k2x 的最大 k k k 值,即寻找最后一个 k 2 k^2 k2小于或等于 x x x 的位置索引。

class Solution {
public:
    // 二分查找
    int mySqrt(int x) {
        int left = 0;
        int right = x;
        int ans = 0;
        while (left <= right){
            int mid = left + (right - left) / 2;
            if ((long long)mid * mid > x){
                right = mid - 1; // 寻找最后一个小于或等于 num 的位置索引
            }
            else{ // (long long)mid * mid <= x
                left = mid + 1;
            }
        }
        return right;
    }
};

367.有效的完全平方数 ●

class Solution {
public:
    bool isPerfectSquare(int num) {
        int left = 0;
        int rigth = num;
        while (left <= rigth){
            int mid = left + (rigth - left) / 2;
            long square = (long) mid * mid;		
            if (square > num){      		// 二分查找条件
                rigth = mid - 1;
            }
            else if (square < num){
                left = mid + 1;
            }
            else{
                return true;
            }
        }
        return false;
    }
};

287. 寻找重复数 ●● ✔

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [ 1, n ] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间
输入:nums = [1,3,4,2,2]
输出:2

1、二分查找

额外空间要求限制了哈希表的使用,可以使用「二分查找」的原因:

因为题目要找的是一个 整数,并且这个整数有明确的范围,所以可以使用「二分查找」。

重点理解:这个问题使用「二分查找」是在构建的虚拟数组 [1, 2,…, n] 中查找一个整数,而 并非在输入数组中查找一个整数。

此处构建虚拟单调有序数组设为cnt[],其值表示 nums 数组中值小于等于索引 index 的数有多少个,以nums = [1,3,4,2,2] 为例,cnt[] 如下表:

index1234
cnt [index]1345
我们的目标就是用二分法在有序数组 [1, n-1] 中找出第一个 cnt 值大于 index 的数
  • 时间复杂度: O ( N l o g N ) O(NlogN) O(NlogN),二分法的时间复杂度为 O(logN),在二分法的内部,执行了一次 for 循环,时间复杂度为 O(N),故时间复杂度为 O(NlogN)。
  • 空间复杂度: O ( 1 ) O(1) O(1),使用了一个 cnt 变量,因此空间复杂度为 O(1)。
class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int n = nums.size();
        int left = 1, right = n - 1;        // 二分法,从1~(len-1)的cnt表中查找,不是从原数组查找
        while(left <= right){
            int mid = left + (right - left) / 2;
            int cnt = 0;
            for(int i = 0; i < n; ++i){     // 遍历判断nums中小于等于mid的个数
                cnt += nums[i] <= mid;
            }
            if(cnt <= mid){        
                left = mid + 1;             // 第一个cnt大于i的数
            }
            else{
                right = mid - 1;    
            }
        } 
        return left;
    }
};
2、快慢指针

将这个题目给的特殊的数组当作一个链表来看,数组的下标就是指向元素的指针,把数组的元素也看作指针。如 0 是指针,指向 nums[0],而 nums[0] 也是指针,指向 nums[nums[0]]。

如果数组中有重复的数,以数组 [1,3,4,2,2] 为例,我们将数组下标 n 和数 nums[n] 建立一个如上所述的映射关系 f ( n ) f(n) f(n),从下标为 0 出发,根据 f(n) 计算出一个值,以这个值为新的下标,再用这个函数计算,以此类推产生一个类似链表一样的序列0->1->3->2->4->2->4->2->……
在这里插入图片描述
从理论上讲,数组中如果有重复的数,那么就会产生多对一的映射,这样,形成的链表就一定会有环路了,

  1. 数组中有一个重复的整数 ==> 链表中存在环
  2. 找到数组中的重复整数 ==> 找到链表的环入口

此时快慢指针解决办法与链表题目环形链表Ⅱ类似。

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int slow = 0, fast = 0;
        while(true){
            slow = nums[slow];
            fast = nums[nums[fast]];
            if(slow == fast){           // 第一次相遇,跳出循环
                break;
            }
        }
        slow = 0;                       // 与起点同时移动
        while(true){
            slow = nums[slow];
            fast = nums[fast];
            if(slow == fast) return slow;   // 再次相遇即为环入口
        }
    }
};
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值