数据结构与算法-数组

一、数组

1、数组基础

在面试中,考察数组的题目一般在思维上都不难,主要是考察对代码的掌控能力。

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

注意:

  • 数组下标都是从0开始的。

  • 数组内存空间的地址是连续的

因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。所以查询操作多、增删操作少的地方可以使用数组。

2、二分查找法

例题:力扣链接

2.1、二分查找法原理

定义查找的范围[left,right],初始查找范围是整个数组nums。每次取查找范围的中点mid,比较数组中点值nums[mid]和目标值target的大小,如果相等则mid即为要寻找的下标,如果不相等则根据nums[mid]target的大小关系将查找范围缩小一半。如此往复,直至找到target

使用二分查找法的前提是数组为有序数组,且数组中无重复元素

2.2、二分查找法难点

二分查找法的难点区间定义不好理解,例如:到底是 while(left < right) 还是 while(left <= right),到底是right = middle呢,还是要right = middle - 1呢?

写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。

2.3、两种写法

第一种方法(左闭右闭)

定义target是在一个在左闭右闭的区间里,也就是[left, right]

此时有如下两点:

  • while (left <= right)要使用<=,因为left == right是有意义的,所以使用<=

  • if (nums[mid] > target)right要赋值为mid - 1,因为当前这个nums[mid]一定不是target,那么接下来要查找的左区间结束下标位置就是mid - 1。同理,if (nums[mid] < target)left要赋值为mid + 1

代码如下:

class Solution {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
            int mid = (right - left) / 2 + left; // 防止溢出 等同于(left + right)/2
            int num = nums[mid];
            if (num == target) {
                return mid; // 数组中找到目标值,直接返回下标
            } else if (num > target) {
                right = mid - 1; // target 在左区间,所以[left, middle - 1]
            } else {
                left = mid + 1; // target 在右区间,所以[middle + 1, right]
            }
        }
        return -1; // 未找到目标值
    }
}

第二种方法(左闭右开)

定义target是在一个在左闭右开的区间里,也就是[left, right)

此时有如下两点:

  • while (left < right),这里使用<,因为left == right在区间[left, right)是没有意义的

  • if (nums[mid] > target)right更新为mid,因为当前nums[mid]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为mid,即:下一个查询区间不会去比较nums[mid]。而由于区间是左闭右开的,因此if (nums[mid] < target)中的left在下一个查询区间仍然会被查询到,所以left要赋值为mid + 1

代码如下:

class Solution {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length; // 定义target在左闭右开的区间里,即:[left, right)
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使                                   用 <
            int mid = (right - left) / 2 + left; // 防止溢出 等同于(left + right)/2
            int num = nums[mid];
            if (num == target) {
                return mid; // 数组中找到目标值,直接返回下标
            } else if (num > target) {
                right = mid; // target 在左区间,在[left, middle)中
            } else {
                left = mid + 1; // target 在右区间,在[middle + 1, right)中
            }
        }
        return -1; // 未找到目标值
    }
}

3、移除元素

例题:力扣链接

数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。

3.1、双指针法

双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

原理:由于题目要求删除数组中等于val的元素,因此输出数组的长度一定小于等于输入数组的长度,我们可以把输出的数组直接写在输入数组上。可以使用双指针:右指针right指向当前将要处理的元素,左指针left指向下一个将要赋值的位置。

如果右指针指向的元素不等于val,它一定是输出数组的一个元素,我们就将右指针指向的元素复制到左指针位置,然后将左右指针同时右移;

如果右指针指向的元素等于val,它不能在输出数组里,此时左指针不动,右指针右移一位。

整个过程保持不变的性质是:区间[0,left)中的元素都不等于val。当左右指针遍历完输入数组以后,left的值就是输出数组的长度。

这样的算法在最坏情况下(输入数组中没有元素等于val),左右指针各遍历了数组一次。

class Solution {
    public int removeElement(int[] nums, int val) {
        int n = nums.length;
        int left = 0;
        for (int right = 0; right < n; right++) {
            if (nums[right] != val) {
                nums[left] = nums[right];
                left++;
            }
        }
        return left;
    }
}

4、有序数组的平方

例题:力扣链接

4.1、暴力解法

每个数平方之后,排个序

class Solution {
    public int[] sortedSquares(int[] nums) {
        int[] ans = new int[nums.length];
        for (int i = 0; i < nums.length; ++i) {
            ans[i] = nums[i] * nums[i];
        }
        Arrays.sort(ans);   //快速排序
        return ans;
    }
}

4.2、双指针法

原理:数组其实是有序的, 只不过负数平方之后可能成为最大数了。那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。此时可以考虑双指针法了,i指向起始位置,j指向终止位置。

定义一个新数组result,和nums数组一样的大小,让k指向result数组终止位置。

如果nums[i] * nums[i] < nums[j] * nums[j] 那么result[k--] = nums[j] * nums[j];

如果nums[i] * nums[i] >= nums[j] * nums[j] 那么result[k--] = nums[i] * nums[i];

如动画所示:

class Solution {
    public int[] sortedSquares(int[] nums) {
​
        int n = nums.length;
        int k = n-1;
        int[] result = new int[n];
        for(int i=0,j=n-1;i<=j;){
            if(nums[i]*nums[i]>=nums[j]*nums[j]){
                result[k--] = nums[i]*nums[i];
                i++;
            }else{
                result[k--] = nums[j]*nums[j];
                j--;
            }
        }
        return result;
​
    }
}

5、长度最小的子数组

例题:力扣链接

5.1、暴力解法

暴力法是最直观的方法。初始化子数组的最小长度为无穷大,枚举数组nums中的每个下标作为子数组的开始下标,对于每个开始下标i,需要找到大于或等于i的最小下标j,使得从nums[i]nums[j]的元素和大于或等于s,并更新子数组的最小长度(此时子数组的长度是j−i+1)。

class Solution {
    public int minSubArrayLen(int s, int[] nums) {
        int n = nums.length;
        if (n == 0) {
            return 0;
        }
        int ans = Integer.MAX_VALUE;   // 最终的结果
        for (int i = 0; i < n; i++) {   // 设置子序列起点为i
            int sum = 0;   // 子序列的数值之和
            for (int j = i; j < n; j++) {   // 设置子序列终止位置为j
                sum += nums[j];
                if (sum >= s) {   // 一旦发现子序列和超过了s,更新ans
                    ans = Math.min(ans, j - i + 1);
                    break;   // 因为是找符合条件最短的子序列,所以一旦符合条件就break
                }
            }
        }
        // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }
}

5.2、滑动窗口

所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果

滑动窗口也可以理解为双指针法的一种。

这里还是以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程:

最后找到 4,3 是最短距离。

实现滑动窗口,主要确定如下三点:

  • 窗口内是什么?

  • 如何移动窗口的起始位置?

  • 如何移动窗口的结束位置?

滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)

原理:定义两个指针leftright分别表示子数组(滑动窗口窗口)的开始位置和结束位置,维护变量sum存储子数组中的元素和(即从nums[left]nums[right]的元素和)。

初始状态下,leftright都指向下标0sum的值为0

每一轮迭代,将nums[right]加到sum,如果sum≥s,则更新子数组的最小长度(此时子数组的长度是right-left+1),然后将nums[left]sum中减去并将left右移,直到sum<s,在此过程中同样更新子数组的最小长度。在每一轮迭代的最后,将right右移。

class Solution {
    // 滑动窗口
    public int minSubArrayLen(int s, int[] nums) {
        int left = 0;   // 滑动窗口起始位置
        int sum = 0;   // 滑动窗口数值之和
        int result = Integer.MAX_VALUE;
        for (int right = 0; right < nums.length; right++) {
            sum += nums[right];
            // 注意这里使用while,每次更新left(起始位置),并不断比较子序列是否符合条件
            while (sum >= s) {
                result = Math.min(result, right - left + 1);
                // 这里体现出滑动窗口的精髓之处,不断变更left(子序列的起始位置)
                sum -= nums[left++];
            }
        }
        // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
        return result == Integer.MAX_VALUE ? 0 : result;
    }
}

6、螺旋矩阵

例题:力扣链接

这道题目可以说在面试中出现频率较高的题目,本题并不涉及到什么算法,就是模拟过程,但却十分考察对代码的掌控能力。

第二节中的二分查找法坚持了循环不变量原则,本体依然要坚持循环不变量原则。

模拟顺时针画矩阵的过程:

  • 填充上行从左到右

  • 填充右列从上到下

  • 填充下行从右到左

  • 填充左列从下到上

这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来。

如图所示:

这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。这也是坚持了每条边左闭右开的原则。

class Solution {
    public int[][] generateMatrix(int n) {
        int[][] res = new int[n][n];
​
        // 循环次数
        int loop = n / 2;
​
        // 定义每次循环起始位置
        int startX = 0;
        int startY = 0;
​
        // 定义偏移量
        int offset = 1;
​
        // 定义填充数字
        int count = 1;
​
        // 定义中间位置
        int mid = n / 2;
        while (loop > 0) {
            int i = startX;
            int j = startY;
​
            // 模拟上侧从左到右
            for (; j<startY + n -offset; j++) {
                res[startX][j] = count++;
            }
​
            // 模拟右侧从上到下
            for (; i<startX + n -offset; i++) {
                res[i][j] = count++;
            }
​
            // 模拟下侧从右到左
            for (; j > startY; j--) {
                res[i][j] = count++;
            }
​
            // 模拟左侧从下到上
            for (; i > startX; i--) {
                res[i][j] = count++;
            }
​
            loop--;
​
            startX += 1;
            startY += 1;
​
            offset += 2;
        }
​
        if (n % 2 == 1) {
            res[mid][mid] = count;
        }
​
        return res;
    }
}

7、数组总结

7.1、数组的理论基础

数组是存放在连续内存空间上的相同类型数据的集合。可以方便的通过下标索引的方式获取到下标下对应的数据。

关于数组需要两点注意的是:

  • 数组下标都是从0开始的。

  • 数组内存空间的地址是连续的

正是因为数组的在内存空间的地址是连续的,所以数组的元素是不能删的,只能覆盖,并且在删除或者增添元素的时候,难免要移动其他元素的地址

7.2、数组的经典题目

1)二分法

二分法要坚持循环不变量原则,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。

二分法是算法面试中的常考题。

2)双指针法

双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。

3)滑动窗口

主要要理解滑动窗口是如何移动窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。

滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)

4)模拟行为

模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,十分考察大家对代码的掌控能力。

在这道题目中,再一次介绍到了循环不变量原则,其实这也是写程序中的重要原则。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值