算法刷题笔记【数组】01

算法刷题笔记【数组】01

数组理论

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

数组可以方便的通过下标索引的方式获取到下标下对应的数据。

注意

  • 数组下标都是从0开始的。
  • 数组内存空间的地址是连续的

因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。

数组例题

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]之间。

分析过程

【文字太长不看系列:↓直接跳到末尾查看打印出循环过程的代码便于理解】

循环不变量

区间定义后,每次改变区间后,区间性质不能变:保持 左闭右闭[a, b] / 左闭右开[a, b),处理规则要按照区间定义来写,使得每次操作的范围划分满足区间的定义。

1.左闭右闭 [a, b]
  • while(left ?? right)

分析:如何判断问号处该使用小于(<)号还是小于等于(<=)号?

例如:当下标区间为[x,x]时,此时区间长度为1,区间内只有一个元素。while判断条件若使用"<=“,则 left=nums[x]right=nums[x],此时条件 left<=right 成立,能使得进入循环的区间满足循环不变;若使用”<",left=nums[x]right=nums[x],此时条件 left<right 不成立,无法使进入循环的区间满足循环不变。

简单来看,闭区间,左边界==右边界是有意义的,因此判断条件可以写成 left <= right

  • target < nums[mid] 时如何更新右边界 right

分析:右边界更新为 mid 还是 mid - 1?

若目标值小于二分后的中值,即 target < nums[mid],则 nums[mid] 一定不在要查找的范围内,不可能为要查找的目标值,即 target!=nums[mid]。此时更新右边界值,right 的正确更新方式为 right = mid - 1,而非 right = mid,否则就将不属于查找范围中的值 nums[mid] 包含了进来。若错误采用了 right < mid 写法,紧接下来的循环可能不会立即导致错误,但当程序运行到二分区间只剩一个元素时,很可能时本该被查到的目标值因为不满足 left < right 条件而退出循环,导致最终找不到目标值。

  • target > nums[mid] 时如何更新左边界 left

分析:左边界更新为 mid 还是 mid - 1?

由右边界更新方式同理可知,nums[mid] 不在区间中,左边界更新只需 left = mid + 1

2.左闭右开[a, b)
  • while(left ?? right)

分析:如何判断问号处该使用小于(<)号还是小于等于(<=)号?

例如:当下标区间为[x,x)时,此时区间长度为1,区间内只有一个元素。while判断条件若使用"<=“,则表示可以有 left=nums[x]right=nums[x],此时条件 left<=right ,不能使得进入循环的区间满足循环不变;若使用”<",left=nums[x]right=nums[x+1],此时条件 left<right 成立,使进入循环的区间满足循环不变。

简单来看,闭区间,**左边界==右边界 **是有意义的,因此判断条件可以写成 left <= right

  • target < nums[mid] 时如何更新右边界 right

分析:右边界更新为 mid 还是 mid - 1?

若目标值小于二分后的中值,即 target < nums[mid],则 nums[mid] 一定不在要查找的范围内,不可能为要查找的目标值,即 target!=nums[mid]。此时更新右边界值,right 可更新为 right = mid ,表示取不到 mid ,而非 right = mid - 1,否则就将应该属于查找范围中的值 nums[mid - 1] 剔除了。

  • target > nums[mid] 时如何更新左边界 left

分析:左边界更新为 mid 还是 mid - 1?

由1中闭区间更新方式,同理可知,nums[mid] 不在区间中,左边界更新仍为 left = mid + 1

小结

将数组区间和数学数列中的区间联系起来也很好理解。

左闭右闭方式,将一个完整区间分为3部分 (<t, =t, >t),最终左右边界会相等,指向最后一次查找的位置;

左闭右开方式,将一个完整区间分为2部分 (<t, >=t),该方式左右边界不会有相等的情况,只会相邻。

题解
左闭右闭(C)
int search(int* nums, int numsSize, int target)
{
    int left = 0, right = numsSize - 1;
    
    while (left <= right) {
        int mid = left + (right - left >> 1);
        
        if (target < nums[mid])
        {
            right = mid - 1;
        }
        else if (target > nums[mid])
        {
            left = mid + 1;
        }
        else
        {
            return mid;
        }
    }
    
    return -1;
}
左闭右开 (C++)
#include <iostream>

class Solution {
    public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size();
        while (left < right) {
            int middle = left + ((right - left) / 2);
            if (target < nums[middle]) {
                left = middle + 1;
            } else if (target > nums[middle]) {
                right = middle;
            } else {
                return middle;
            }
        }
        return -1;
    }
};
打印出二分过程(C语言版)
/************************************************************************
> File Name:	0704_binarySearch.c
> Author:		leon-ais
> Mail:			leon_ais@qq.com
> Created Time:	 Wed 21 Sep 2022 05:43:48 PM CST
> Description:	
************************************************************************/
#include <stdio.h>

// 辅助函数用于打印数组
void show(int *a, int n) {
    printf("arr=[");
    for (int i = 0; i < n; ++i) {
        printf("%d", a[i]);
        if (i != n - 1) {
            printf(" ");
        }
    }
    printf("]\n");
}

// 辅助函数用于打印二分过程
void msg(int* a, int n, int l, int m, int r) {
        show(a, n);
        if (l == m || m == r) {
            printf("loop:");
            printf("%*sl\n", l*2, "");
            printf("loop:");
            printf("%*sm\n", m*2, "");
            printf("loop:");
            printf("%*sr\n", r*2, "");
        } else {
            printf("loop:");
            printf("%*sl", l*2, "");
            printf("%*sm", m*2-l*2-1, "");
            printf("%*sr\n", r*2-(m*2)-1, "");
        }
        printf("l=%d m=%d r=%d\n", l, m, r);
}

int search(int* a, int n, int x) {

    int l = 0, r = n - 1;

    while (l <= r) {
        int m = l + (r - l >> 1);
        msg(a, n, l, m, r);
        if (x < a[m]) {
            r = m - 1;
        } else if (x > a[m]) {
            l = m + 1;
        } else {
            return m;
        }
    } 

    return -1;
}

int main(int argc,char* argv[]) {

    int a[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    int n = 10;
    int x = 3;

    // show(a, n);

    int i = search(a, n, x);
    
    printf("\nThe value \"%d\" is at a[%d].\n", x, i);

    return 0;
}

// 程序运行结果

Program returned: 0
Program stdout
arr=[0 1 2 3 4 5 6 7 8 9]
loop:l       m         r
l=0 m=4 r=9
arr=[0 1 2 3 4 5 6 7 8 9]
loop:l m   r
l=0 m=1 r=3
arr=[0 1 2 3 4 5 6 7 8 9]
loop:    l
loop:    m
loop:      r
l=2 m=2 r=3
arr=[0 1 2 3 4 5 6 7 8 9]
loop:      l
loop:      m
loop:      r
l=3 m=3 r=3

The value "3" is at a[3].
细节处理
求平均数防止溢出
问题背景:
二分法每次更新mid需要除以2
有可能溢出的写法:
mid = (left+right) / 2;
原因分析:
当给定的数组长度很长,待查找元素的位置靠近数组尾部时,当循环快结束时,left和right可能都会很大,还没等除以2求得平均值,left+right求和的值就已经超出了int最大范围,导致溢出错误。
加减运算符和右移运算符的优先级
问题背景:
二分法每次更新mid需要除以2
错误写法:
mid = left+(right-left) >> 1;
原因分析:
右移运算符>>的优先级不如加减法+-高,先计算加减法,再进行移位,错误写法等价于(left+(right-left))/2;
则在不确定符号优先级时,尽量使用圆括号()维护运算顺序。
正确写法:
mid = left+(right-left >> 1);或mid = left+((right-left) >> 1);

OJ术语
OJ术语:AC、WA、RE、CE、TLE、MLE、PE、OLE、分别什么意思
‌AC
Accepted 答案正确/通过
‌WA
Wrong Answer 答案错误
‌RE
Runtime Error 运行时错误
‌CE
Complie Error 编译错误
‌TLE
Time Limit Exceed 超出时间限制/超时
‌MLE
Memory Limit Exceed 超出内存限制
‌PE
Presentation Error 格式错误
‌OLE
Output Limit Exceed 输出超出限制/输出超限
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值