[ 数据结构菜鸟教程 - I ] 你可能需要重新学习下数组

127 阅读6分钟

# 前言

下面我们开始第一课啦。

在每一节的内容中我将会先讲解一些基本的知识点,然后会提出一些经典题型帮助大家入门。

在这个过程中我也会不断迭代我创作内容的形式和方法,和大家一起探索,如何可以简单,轻松上手入门数据结构与算法。

# 数组基础知识

  1. 数组的定义我们并不陌生,它包括一个索引来指定数组中元素的位置,在常见的编程语言 JavaScript, Java 中我们的索引都是从0开始的。也就是说 a[3] 对应的是数组中的第四个元素。

  2. 数组的特点是支持随机访问。也就是说我们可以使用数组的索引来访问固定位置的元素,比如 a[0]表示访问数组第一个元素。这一个特点使得数组在提高时间复杂度的时候会有很巧妙的作用,我们接下来会提到这个用法。

  3. 数组的关键是,索引和寻址。也就是我们如何去管理数组的索引值,有的时候索引值使用的巧妙,会为我们巧妙的解决问题提供帮助,下面的例题讲解部分,大家会有体会。

  4. 数组的基本操作包括以及对应的时间复杂度

    标题时间复杂度
    查询O(1)
    插入O(n)
    删除O(n)
    末尾插入O(1)
    头部插入O(n)
  5. 变长数组

    实现的方法是初始阶段创建一个空数组,分配一个预估值的常数作为数组大小。下面是操作变长数组过程中会遇到的两种情形:

    1. 在插入元素的情况下,如果空间不够,我们需要重新申请2倍大小的空间,把数组的内容拷贝到新的空间,释放旧空间
    2. 在弹出元素的情况下,如果空间利用率不到 25%,则释放一半的空间

# 实战环节

  1. 88 合并两个有序数组

    给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
    请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
    注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
    
    示例 1:
        输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
        输出:[1,2,2,3,5,6]
        解释:需要合并 [1,2,3] 和 [2,5,6] 。
        合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
    
    示例 2:
        输入:nums1 = [1], m = 1, nums2 = [], n = 0
        输出:[1]
        解释:需要合并 [1] 和 [] 。
        合并结果是 [1] 。
    
    示例 3:
        输入:nums1 = [0], m = 0, nums2 = [1], n = 1
        输出:[1]
        解释:需要合并的数组是 [] 和 [1] 。
        合并结果是 [1] 。
        注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。
        
    提示:
        nums1.length == m + n
        nums2.length == n
        0 <= m, n <= 200
        1 <= m + n <= 200
        -109 <= nums1[i], nums2[j] <= 109
        
    进阶:
        你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?
    

    这个题目中明确指出了 nums1 的空间是 m + n,是足够承载我们处理后数组的空间大小的。所以我们可以在nums1的空间上完成对应操作。我们可以创建三个索引,分别对应第一个数组的末尾索引,第二个数组的末尾索引,还有最后处理好的数组末尾索引。我们在这个过程中不断的移动处理好数组末尾的指针。最后别忘了处理后面数组剩下的元素。这样的话可以保证我们最后的时间复杂度是 O(1)。

    class Solution {
        public void merge(int[] nums1, int m, int[] nums2, int n) {
            int tail1 = m - 1, tail2 = n - 1, pointer = m + n - 1;
    
            while(tail1 >= 0 && tail2 >= 0) {
                if(nums1[tail1] > nums2[tail2]) {
                    nums1[pointer] = nums1[tail1];
                    pointer--;
                    tail1--;
                } else {
                    nums1[pointer] = nums2[tail2];
                    pointer--;
                    tail2--;
                }
            }
    
            while(tail2 >= 0) {
                nums1[pointer] = nums2[tail2];
                pointer--;
                tail2--;
            }
        }
    }
    // Time complexity : O(n) , n is the length of the array
    // Space complexity : O(1)
    
  2. 26. 删除有序数组中的重复项

    给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
    
    考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
    
    更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
    返回 k 。
    
    示例 1:
        输入:nums = [1,1,2]
        输出:2, nums = [1,2,_]
        解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
    
    示例 2:
        输入:nums = [0,0,1,1,1,2,2,3,3,4]
        输出:5, nums = [0,1,2,3,4]
        解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
    
    提示:
        1 <= nums.length <= 3 * 104
        -104 <= nums[i] <= 104
        nums 已按 非严格递增 排列
    

    这个题目的解法我们可以使用经典的 双指针。针对这个题目我们可以创建一个指针始终指向下一个非重复元素,如果我们在遍历的过程中发现有重复元素,那么我们更新这个指针。

    class Solution {
        public int removeDuplicates(int[] nums) {
            if(nums.length == 0) return 0;
            int i = 0;
            for(int j = 0; j < nums.length; j++){
                if(nums[j] != nums[i]){
                    i++;
                    nums[i] = nums[j];
                }
            }
            return i+1;
        }
    }
    
    // Time complexity : O(n) , n is the length of the array
    // Space complexity : O(1)
    
  3. 283. 移动零

    给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
    请注意 ,必须在不复制数组的情况下原地对数组进行操作。
    
    示例 1:
        输入: nums = [0,1,0,3,12]
        输出: [1,3,12,0,0]
    
    示例 2:
        输入: nums = [0]
        输出: [0]
    
    提示:
        1 <= nums.length <= 104
        -231 <= nums[i] <= 231 - 1
    
    进阶:你能尽量减少完成的操作次数吗?
    

    这个题的解法我们可以继续使用双指针,其中 left 指针指向用来指向下一个非零元素,right 指针用来遍历完整数组。最后不要忘记把处理完数组的最后元素都补成0。

    class Solution {
        public void moveZeroes(int[] nums) {
            int left = 0, right = 0;
            while(right < nums.length) {
                if(nums[right] != 0) {
                    nums[left] = nums[right];
                    left++;
                }
                right++;
            }
    
            while(left < nums.length) {
                nums[left] = 0;
                left++;
            }
        }
    }