代码随想录算法训练营第一天 | LeetCode704 二分查找、LeetCode27 移除元素

LeetCode 704 二分查找

题目链接:704.二分查找

题目简述:

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

思路:

先用一种最简单的方法,基本上是刚刚接触编程语言就会使用的算法(for循环依次遍历)

int search(int* nums, int numsSize, int target) {
    int targetIndex = -1;
    for(int i = 0; i < numsSize; i++){
        if(nums[i] == target){
            targetIndex = i;
            break;
        }
    }
    return targetIndex;
}

虽然也能AC,但是要想想是不是有更高效的算法?

这就用到题目中所说的二分查找。

但要注意,在任何使用二分法思想进行查找的情况下(比如折半查找,二叉排序树等),这个数据结构(顺序表、数组)都必须是有序的。反过来说,有序也是使用二分查找法的原因,这样会使程序更加的高效。

以下是二分查找法的代码:

int search(int* nums, int numsSize, int target) {
    int targetIndex = -1;
    int head = 0, tail = numsSize - 1;
    while(head <= tail){
        int mid = (head + tail) / 2;
        if(target < nums[mid]){
            tail = mid - 1;
        }
        else if(target > nums[mid]){
            head = mid + 1;
        }
        else{
            targetIndex = mid;
            break;
        }
    }
    return targetIndex;
}

虽然说这个题看上去简单,但是对于初学者或者对二分法不熟练的同学来说,还是有很多需要注意的地方:

(1)while循环的条件,应该是head <= tail,中间是小于等于,像我在初学的时候,放了很多样例都是正确,但是没有完全AC,这时就得考虑多种情况,也就是head与tail可以相等吗,也就是说,当两者指向同一位置的元素时是否退出循环。

         读者如果仔细想一想,答案是肯定的,比如,如果target值与首元素或尾元素相等,就得必须查找到两个指针全都指向首(尾)元素。另外,这种情况下其实也是二分查找效率最低的情况。

(2)还有一个小的注意点,在我刚开始学的时候犯过这种错误,就是while循环里面,这三个判断语句,如果想简单第三个判断语句想用else,那么第二个判断语句的必须得是else if,有些粗心的同学(比如我),就直接两个if 、一个else,显然判断就会出错,这样就会导致else语句和第一个if语句无关联。

以下是关于二分查找法的拓展题,更有难度,但是对提高对二分查找区间的理解有很大作用!

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

题目链接:34.在排序数组中查找元素的第一个和最后一个位置

通过此题,会对二分查找的区间和查找遍历数组的过程有了更深的了解:


    通过二分查找,分别求得左边界和右边界,左右边界的查找大致过程相同,只是在left和right循环改变上有些不同


   (1)在查找左边界时,通过右指针的不断靠近获得(不论是原条件目标值小于中间值还是等于改变右指针进行左移靠近寻找),最后的right的落点在最左边目标元素的左边(最后判断条件为相等,还是改变right值 - 1),使得left > right成立,循环结束。


      (2)查找右边界同理。


所以最后的出来的区间相当于 左开右开 (leftBorder, rightBorder)的区间,两边值都取不到。

而在解题的过程中也要考虑三种情况,两种是不存在的:


(1)目标值不存在数组中,并且小于数组最小值,或者大于数组最大值(也就是说,在数组的两边),最后循环也会循环到数组的两边。


(2)目标值不存在数组中,但是目标值在数组中间。


(3)目标值存在数组中。

还有非常重要的一个细节就是,leftBorder和rightBorder的初始值为什么是 -2,为什么不是 -1?
由于区间是左开右开的区间,而且,数据结构是数组,0号可以取到,就要想到如果目标值的左边界在0号元素(此时其实数组中存在目标值元素),那么他的左边界leftBorder的值可能为 - 1,由于初始值是用来判断数组中是否存在目标值元素的,所以用-2(不可能被取到的值)。

#include <stdio.h>
#include <string.h>

int getRightBorder(int *nums, int numsSize, int target){
    int left = 0;
    int right = numsSize - 1;
    int rightBorder = -2;
    while(left <= right){
        int middle = left + ((right - left) / 2);
        if(target < nums[middle]){
            right = middle - 1;
        }
        else{
            left = middle + 1;
            rightBorder = left;
        }
    }
    return rightBorder;
}
int getLeftBorder(int *nums, int numsSize, int target){
    int left = 0;
    int right = numsSize - 1;
    int leftBorder = -2;
    while(left <= right){
        int middle = left + ((right - left) / 2);
        if(target > nums[middle]){
            left = middle + 1;
        }
        else{
            right = middle - 1;
            leftBorder = right;
        }
    }
    return leftBorder;
}
int* searchRange(int* nums, int numsSize, int target, int* returnSize) {
    int leftBorder =getLeftBorder (nums, numsSize, target);
    int rightBorder = getRightBorder(nums, numsSize, target);
    printf("%d %d\n", leftBorder, rightBorder);
    int *re;
    re = (int *)malloc(sizeof(int) * 2);
    *returnSize = 2;
    re[0] = -1;
    re[1] = -1;
   //第一种情况:target在数组的两边
   if(rightBorder == -2 || leftBorder == -2){
        return re;
    }
    //第三种情况:存在target
    if(rightBorder - leftBorder > 1){
        re[0] = leftBorder + 1;
        re[1] = rightBorder - 1;
        return re;
    }
    //最后一种情况:在数组的中间
    return re;
}

int main(){
    int nums[] = {1, 3, 3, 3, 7, 10};
    int numsSize = 6;
    int *re;
    int *returnSize;
    int target;
    while(scanf("%d", &target) != EOF){
        returnSize = (int*)malloc(sizeof(int));
        //re = (int*)malloc(sizeof(int) * 2);
        re = searchRange(nums, numsSize, target, returnSize);
        printf("%d %d", re[0], re[1]);
    }
}

以下是给大家的一些测试样例:

3
0 4
1 3
2  //在数组中间,且不存在
0 1
-1 -1
-1//在数组左边,不存在
-1 -2
-1 -1
20//在数组右边,不存在
-2 6
-1 -1

由实验结果可知,当目标值target在数组两边时,


       (1)当target在数组左边时,查找左边界和右边界的循环分别遍历到,左指针未被改变过(仍未-2),右指针一直遍历到数组最小下标的右边(一般为-1);


       (2)当target在数组右边时,右指针没有被改变过(为-2),左指针一直寻找到数组最右边(最大下标+1)。


        注意:以上两点中的左右指针的值不是同一个循环的,而是分别在寻找左右两边界的不同函数中。

LeetCode27 移除元素

题目链接:27.移除元素

题目简述:

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

思路:

void moveElement(int *nums, int startIndex, int numsSize){
    for(int i = startIndex; i < numsSize - 1; i++){
        nums[i] = nums[i + 1];
    }
}
int removeElement(int* nums, int numsSize, int val) {
    int cnt = numsSize;
    for(int i = 0; i < cnt; i++){
        if(nums[i] == val){
            cnt--;
            moveElement(nums, i, numsSize);//模块化
            i--;
        }
    }
    return cnt;
}

这个原地修改的算法也挺简单,只是要注意,在removeElement函数中,移动元素之后,一定要进行 i-- 的操作,不然就会漏判删除元素的下一个元素(此元素已经移动到被删除元素的位置)。

上面是在看代码随想录之前的算法思路,接下来是看完代码随想录之后的思路(双指针法):

先来讲一下我对双指针法的理解,也就是快慢指针法,通过快指针遍历循环一次,在某种条件下,慢指针不动,只有快指针改变。简单来说就是设置一个fast,一个slow,由fast快指针遍历一遍数组。

int removeElement(int* nums, int numsSize, int val) {
    int fast = 0, slow = 0;
    for(; fast < numsSize; fast++){
        if(nums[fast] != val){
            nums[slow] = nums[fast];
            slow++;
        }
    }  
    return slow;
}

还有一种是我自己改写的双指针算法:(设置前后两个指针遍历,时间复杂度上是一样的,都是遍历一遍数组):

int removeElement(int* nums, int numsSize, int val) {
    int i = 0, j = numsSize - 1;
    for(; i <= j; ){
        if(nums[i] == val){
            if(nums[j] != val){
                nums[i++] = nums[j--];
            }
            else{
                j--;continue;
            }
        }
        else i++;
    }
    return i;
}

双指针法将对数组的遍历替换的时间复杂度都降到了O(n)。

今日总结:

1.文章链接和视频链接内容很丰富,今天因为课多,简单的看了一下,明天准备认真研究。

2.在手机上看到题目的第一想法是应该挺简单的,很快能做出来,因为这些算法以前都学过。

3.这两个题虽然简单但是确实有一些小的细节问题应该注意,也是体现了自己的不太熟练,在算法设计的过程中应该充分考虑到在遍历过程中的一些变量(比如数组)的改变是否会影响以后(下一次循环遍历)的判断(比如27.移除元素的i--)。

    还有一个需要注意的就是二分查找的循环遍历的条件,考虑到多种情况,并且在设计过程中充分考虑算法的时间复杂度。

第二十二天的算法训练营主要涵盖了Leetcode题目中的三道题目,分别是Leetcode 28 "Find the Index of the First Occurrence in a String",Leetcode 977 "有序数组的平方",和Leetcode 209 "长度最小的子数组"。 首先是Leetcode 28题,题目要求在给定的字符串中找到第一个出现的字符的索引。思路是使用双指针来遍历字符串,一个指向字符串的开头,另一个指向字符串的结尾。通过比较两个指针所指向的字符是否相等来判断是否找到了第一个出现的字符。具体实现的代码如下: ```python def findIndex(self, s: str) -> int: left = 0 right = len(s) - 1 while left <= right: if s[left == s[right]: return left left += 1 right -= 1 return -1 ``` 接下来是Leetcode 977题,题目要求对给定的有序数组中的元素进行平方,并按照非递减的顺序返回结果。这里由于数组已经是有序的,所以可以使用双指针的方法来解决问题。一个指针指向数组的开头,另一个指针指向数组的末尾。通过比较两个指针所指向的元素的绝对值的大小来确定哪个元素的平方应该放在结果数组的末尾。具体实现的代码如下: ```python def sortedSquares(self, nums: List[int]) -> List[int]: left = 0 right = len(nums) - 1 ans = [] while left <= right: if abs(nums[left]) >= abs(nums[right]): ans.append(nums[left ** 2) left += 1 else: ans.append(nums[right ** 2) right -= 1 return ans[::-1] ``` 最后是Leetcode 209题,题目要求在给定的数组中找到长度最小的子数组,
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值