数据结构算法——数组(一)| 青训营笔记

87 阅读4分钟

这是我参与「第四届青训营 」笔记创作活动的第9天

前言

正值8.9月份,各互联网大厂的秋招季在火爆开展,非常希望能进个中大厂,于是现在就用蹒跚的步伐在此开展数据结构算法的学习记录啦

一、走进数组

1、数组下标默认总是从0开始

2、因为内存空间地址是连续的,所以数组元素不能删,只能覆盖。即不能释放单一元素,如果要释放,就是全释放

3、数组的length属性

length属性不计算非整数的键,length值取最大的正整数键值+1,而不是键值对个数

举个栗子:

image.png image.png

解释:a.length取最大的正整数键值‘5’+1 = 6,所以length值为6

 

下面是重头菜:关于数组经典算法题的个人见解和记录啦

 

二、二分查找

力扣原题链接

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

image.png

提示:

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

本题可以使用二分法的前提:

①有序数组(如果是无序的,可以先排序,但要注意排序的成本)

②无重复数组(一旦有重复元素,二分查找法返回的元素下标可能不唯一)

使用二分法的关键点:确定区间!!

①左闭右闭 [left , right]

②左闭右开 [left , right)

①左闭右闭[ left,right ]:

// 二分法--左闭右闭
var search = function(nums, target) {
    //初始
    let left = 0,
        right = nums.length-1
    //进入循环
    while(left <= right){
        let mid = left + Math.floor( (right - left)/2 )
        if(nums[mid] < target){
            //向右查
            left = mid + 1
        }
        else if(nums[mid] > target){
            //向左查
            right = mid - 1
        }
        else{
            //nums[mid] == target!! 找到当前值下标,返回
            return mid
        }
    }
    //退出循环
    return -1 
}

解释:

1、初始: left = 0 //因为数组的存储方式,下标从0开始

2、进入循环条件:while( left <= right ) // 因为区间[ left ,right ]代表了left == right时有意义,所以当 left == right 时也要进入循环。

3、向右查:left = mid + 1 // 为什么要加一?因为进入if的条件就是target大于nums[mid],而不是大于等于,mid这个下标的元素不会等于target,所以left要等于mid+1

4、向左查:right = mid -1 //为什么要减一?同理“向右查”,target是小于nums[mid],不是小于等于

5、返回当前值下标:return mid //进入到最后的else,代表nums[mid] == target啦

6、退出循环:return -1 //如果进入到循环的最后一次遍历,是‘left == right’,即只有一个值需要与target对比,如果target仍然没有进入最后的else‘return mid’,代表target未在数组nums中存在,所以返回-1

 

②左闭右开[ left ,right ):

//二分法--左闭右开
var search = function(nums, target) {
    //初始
    let left = 0,
        right = nums.length
    //进入循环
    while(left < right){
        let mid = left + Math.floor( (right - left)/2 )
        if(nums[mid] < target){
            //向右查
            left = mid + 1
        }
        else if(nums[mid] > target){
            //向左查
            right = mid
        }
        else{
            //nums[mid] == target!! 找到当前值下标,返回
            return mid
        }
    }
    //退出循环
    return -1 
}

一个小疑问:区别于上一个左闭右闭[ left,right ]的区间,左闭右开[ left ,right )有什么区别呢?为什么要这么设置?

带着小疑问往下寻找答案吧~

1、初始:right = nums.length // 因为左闭右开[ left ,right )中,right等于left没有意义,所以right的取值就是“最大可取的下标 + 1”。最开始的初始化,最大可取下标是nums.length -1,所以right = ( nums.length -1 ) + 1 = nums.length

2、进入循环:while(left < right) // 左闭右开[ left ,right ),right等于left没有意义

3、向左查:right = mid //为什么不是mid-1了呢?可以回看第一点‘right的取值就是“最大可取的下标 + 1”’。当进入nums[mid] > target条件语句中,表示mid不会是target的下标了,最大可取下标是mid-1,所以right = (mid - 1)+ 1 = mid

整理一下:

[ left,right ]:

初始条件:left = 0,right = arr.length -1

终止循环:left >= right

向左查:right = mid - 1

向右查:left = mid + 1

[ left ,right ):

初始条件:left = 0,right = arr.length

终止循环:left > right

向左查:right = mid

向右查:left = mid + 1

 

三、移除元素

力扣原题链接

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

image.png

提示:

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

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

 

①本题使用“双指针”(又叫快慢指针)可以实现‘不借助额外的数组空间,实现原地移除’

//移除元素--双指针
var removeElement = (nums, val) => {
    let left = 0
    for(let right = 0; right< nums.length; right++){
        //判断nums[right]
        if(nums[right] != val){
            nums[left++] = nums[right]
        }
    }
    return left
}

解释:

1、通过一个快指针和一个慢指针在一个for循环下完成两个for循环的工作!!

2、先初始化:左右指针最开始指向0下标

3、进入循环:判断nums[right]  // 如果右指针指向的元素不等于val,那么它就一定是输出数组中的一个元素,①于是将右指针指向的值赋给左指针,②左右指针同时右移。

如果nums[right] == val,那么这个值需要去除,right右指针右移,left左指针不动,表示[ 0,left )区间内没有该值

4、区间[ 0,left) 中的元素都不等于val。当左右指针遍历完输入的数组后,left的值就是输出数组的长度,[ 0,left )就是即将输出的数组。

来个小疑问:为什么输出的数组区间不是[ 0, left ]呢?

因为当right右指针进入最后一次if循环内,右指针的值赋给了左指针后,左指针自加一,即,此时左指针指向的下标是将要输出数组的最后一个下标加一,所以真正输出的是[ 0,left )数组,长度是left

复杂度分析:

时间复杂度O(n):n为序列长度,我们只需要遍历该序列至多两次

空间复杂度O(1):我们只需要常数的空间保存若干变量

 

②“优化方案”:同样利用双指针

//移除元素--双指针优化
var removeElement2 = (nums, val) => {
    let left = 0,right = nums.length-1
    while(left<=right){
        //判断nums[left]
        if(nums[left] == val){
            nums[left] = nums[right--]
        }else{
            left++
        }
    }
    return left
}

与上面的双指针解题区别在于:

1、初始化时,左指针同样指向0下标,右指针指向了最后一个下标。

2、进入循环的次数好像减半了,上面题解是最坏的情况是左右指针分别遍历了该序列一次,即最多遍历两次,而本题解的左右指针分别从数组的首尾出发向中间靠拢,所以至多是遍历一次序列。

3、进入while循环的条件是left<=right,为什么不是left<right? 因为left 等于right时也有效,即该值也需要if判断

4、上面题解与val判断的是右指针right,本题与val判断的是左指针left。即,直接由左指针判断,无须经过右指针判断后再赋值给左指针

其他:

1、If...else逻辑:如果左指针left指向的值 == val,那么将右指针指向的值覆盖左指针的值(下一次循环就是判断该值,即右指针赋给左指针的值),然后右指针左移 -- ,否则左指针left指向的值 !=val ,那么左指针指向的值保留(不用让右指针的值覆盖左指针的值) ,左指针右移 ++。

2、与上题解同,输出的数组是[ 0,left ),输出的数组长度是left

未完待续...