数组

大家好,我是老马。

今天我们一起来学习一下数组这种数据结构。

主要知识

数组需要拆分下面几个部分:

  1. 理论介绍

  2. 源码分析

  3. 数据结构实现?

  4. 题目练习(按照算法思想分类)

  5. 梳理对应的 sdk 包

  6. 应用实战

因为这个是 leetcode 系列,所以重点是 4、5(对4再一次总结)。

为了照顾没有基础的小伙伴,会简单介绍一下1的基础理论。

简单介绍1,重点为4。其他不是本系列的重点。

T1089 复写零

给你一个长度固定的整数数组 arr ,请你将该数组中出现的每个零都复写一遍,并将其余的元素向右平移。

注意:请不要在超过该数组长度的位置写入元素。请对输入的数组 就地 进行上述修改,不要从函数返回任何东西。

示例 1:

输入:arr = [1,0,2,3,0,4,5,0] 输出:[1,0,0,2,3,0,0,4] 解释:调用函数后,输入的数组将被修改为:[1,0,0,2,3,0,0,4]

示例 2:

输入:arr = [1,2,3] 输出:[1,2,3] 解释:调用函数后,输入的数组将被修改为:[1,2,3]

提示:

1 <= arr.length <= 104 0 <= arr[i] <= 9

v1-暴力方式

思路

就是最朴素的符合题目意思的解法,没有任何技巧。

解法

    public void duplicateZeros(int[] arr) {
        int n = arr.length;
        for(int i = 0; i < n; i++) {
            if(arr[i] == 0) {
                // 当前位置后面的全部往后移动一个位置
                for(int j = n-1; j >= i+1; j--) {
                    arr[j] = arr[j-1];
                }
                // 下一个位置插入0
                i++;
                if(i >= n) {
                    return;
                }
                arr[i] = 0;
            }
            
            // 提前结束
            if(i >= n) {
                return;
            }
        }
    }

效果

17ms 击败 6.70%

竟然 AC 了,大概这就是 easy 的理由。

v2-空间换时间

思路

虽然这一题要求原地排序,不过我们用这个方法用来作为双指针的过渡。

left 对应 temp 数字下标。

right 对应原始的下标。

我们可以用 left right 两个指针,遇到 0 就往 left 多加一个0。

如果 left 长度和以前一样,结束。

实现

    public void duplicateZeros(int[] arr) {
        int n = arr.length;
        int left = 0;
        int right = 0;

        // 临时数组
        int[] temp = new int[n];
        for(right = 0; right < n; right++) {
            temp[left++] = arr[right];
            // 提前结束
            if(left >= n) {
                break;
            }

            // 处理0
            if(arr[right] == 0) {
                temp[left++] = 0;
                if(left >= n) {
                    break;
                }
            }
        }
        
        // copy
        System.arraycopy(temp, 0, arr, 0, n);
    }

效果

0ms 100%

反思

当然,这种方法我们开辟了新的空间,实际上是不符合题意的。

这就是题目设计的不够开放的问题,可以把原地当做一个额外的限制条件。而不是上来就限定死。

v3-双指针

思路

其实这一题是比较难的,双指针拐了2次弯儿,一般人10分钟内估计根本想不到。

就算想到了用 left、right 两个指针移动,类似于 T26 T27,但是会发现,因为要复写 0,会导致后面的数字被覆盖,这个和移除是不同的。

那么,怎么解决呢?

逆流而上

最核心的问题在于直接 0 复制,会导致后面的信息被覆盖。

所以我们最好是从后往前操作。

自然地,我们可以想到快慢指针,先找到结束的位置。然后倒过来处理一遍。

不过做的时候发现好多坑…

实现

错误的解法1

比如下面的解法,虽然考虑到了最后一个数字可能为0.是否需要重复的标识。

    public static void duplicateZeros(int[] arr) {
        int n = arr.length;

        // 通过快慢指针,找到结束时 left 指针的位置。
        // 同时记录最后一个 0 是否需要复制
        boolean lastZeroRepeatFlag = true;
        int totalCount = 0;
        int left = 0;
        for(int i = 0; i < n; i++) {
            left++;
            totalCount++;
            if(totalCount >= n) {
                break;
            }
            if(arr[i] == 0) {
                totalCount++;
                // 如果最后一个0是因为刚好0复写变满了,那么则不需要复制。
                if(totalCount >= n) {
                    lastZeroRepeatFlag = false;
                    break;
                }
            }
        }

        // 从哪里开始写
        // 从后往前,最后一个位置开始
        int i = n-1;
        int leftIx = left - 1;
        while (i >= 0 && leftIx >= 0) {
            // 当前位置
            arr[i--] = arr[leftIx];

            // 当前位置是0
            if(arr[leftIx] == 0 && i > 0) {
                // 两个场景需要复制 0
                //a. 不是最后一个0
                //b. 最后一个0 lastZeroRepeatFlag=true
                if(leftIx != (left-1)
                    || (leftIx == left-1 && lastZeroRepeatFlag)) {
                    arr[i--] = 0;
                }
            }

            leftIx--;
        }
    }

但是在测试用例

输入 arr = [8,4,5,0,0,0,0,7]

输出 [0,0,0,0,0,0,0,0]

预期结果 [8,4,5,0,0,0,0,0]

这个用例却过不去。

正确解法

这里还是看了三叶的解法,很优秀。

三叶的解法

这种写法真的很优雅。

实现如下:

    public static void duplicateZeros(int[] arr) {
        int n = arr.length;

        // 慢指针
        int i = 0;
        // 快指针,包含复写0的数据
        int j = 0;
        // 这个跳出写法,比我的优雅很多
        while (j < n) {
            if(arr[i] == 0) {
                j++;
            }
            i++;
            j++;
        }

        // 此时,位置实际上在预期的后面一位
        i--;
        j--;

        //2.从尾到头进行双指针赋值 i : i->slow   j : n->fast
        while (i >= 0) {
            // j最后一步可能执行了+2操作,在此确保j的坐标小于n
            if(j < n) {
                arr[j] = arr[i];
            }

            //判断是否需要复写0
            if(arr[i] == 0 && j > 0) {
                arr[--j] = 0;
            }

            // 同时左移
            i--;
            j--;
        }
    }

反思

虽然看出来是双指针,但是无可奈何,依然没有写对。

有进步,但是还不够。

加油!


数组基本知识+技巧

分类 代表技巧
遍历类 遍历、双指针、滑窗
前缀/差分类 前缀和、差分、后缀数组
查找类 二分、哈希、离散化
空间/状态优化 状压、滚动数组、差分压缩
排序/归并类 排序、归并逆序对、树状数组、线段树
子数组/序列 动规、中心扩展、单调栈
矩阵类 二维差分、DFS/BFS、旋转模拟
特殊技巧 快速幂、滚动哈希、模拟题技巧

🎯 常见遍历技巧 + 力扣题目对照表

技巧 力扣题目 说明
基础遍历 27.移除元素 直接遍历+判断
快慢指针 26. 删除重复项 原地去重
左右夹逼 167.两数之和 II 排序+夹逼
滑动窗口 209. 长度最小子数组 动态控制窗口
子数组枚举 560. 和为K的子数组 前缀和优化
子序列枚举 491. 递增子序列 回溯
倒序遍历 198. 打家劫舍 动态规划
规则模拟 54. 螺旋矩阵 控制方向循环遍历