算法通关村第三关——爱不起的数组与双指针思想(青铜)

1. 什么是数组

数组:存储一组相同数据类型的数据的集合。

注意事项: 在 Java 中, 数组中包含的变量必须是 相同类型.

2. 使用数组

数组的使用方法无非就是四个步骤:声明数组、分配空间、赋值、处理。

  1. 声明数组:就是告诉计算机数组的类型是什么。有两种形式:int[] array、int array[]。

  2. 分配空间:告诉计算机需要给该数组分配多少连续的空间,记住是连续的。array = new int[10];

  3. 赋值:赋值就是在已经分配的空间里面放入数据。array[0] = 1 、array[1] = 2……其实分配空间和赋值是一起进行的,也就是完成数组的初始化。有如下三种形式:

2.1 创建数组(声明数组)

基本语法:

// 动态初始化
数据类型[] 数组名称 = new 数据类型 [] { 初始化数据 };
// 静态初始化
数据类型[] 数组名称 = { 初始化数据 };

代码示例:

int[] arr = new int[]{1, 2, 3};
int[] arr = {1, 2, 3};
int arr[] = {1, 2, 3};//和 C语言更相似了.但是我们还是更推荐写成 int[] arr 的形式. int和 [] 是一个整体.
int[] array = {1,2,3,4,5,6,7};//定一个数组并初始化
int[] array2 = new int[3];//代表我们创建了一个可以存放3个整形的数组 默认值为全0
int[] array2 = new int[]{1,2,3,4,5};//代表我们创建了一个可以存放5个整形的数组 并初始化为1 2 3 4 5

2.2 分配空间

每一个软件都占用一定的内存空间。

img

img

栈与堆

img

java 内存分配

int变量例子

int等类型的变量无new关键字,不在堆中开辟空间,值直接在栈中赋给变量名。
img

2.3 数组赋值(内存显示)

数组的初始化则涉及到在堆内存中开辟新的空间(静态初始化的简写,eg:int[] array = {1,2,3},是省略了new int[]的,因此静态初始化也涉及开辟新的空间)。因此实际传递给变量名的实际上是在堆内存中的地址,而非是直接的数值

在堆中新建的不同的数组互相独立。
img

但实际上,也存在两个数组指向同一个空间的内存图。
img

没有通过索引赋值,而是直接将arr1中记录的地址传递给了arr2,那么两个array指向的就是同一个堆中的空间。此时其中一个数组对内存中的值做了更新,那么通过另一个变量名进行访问的时候,得到的也是更新了的值。

3. 数组的使用(增删改查)

3.1 查找一个元素

为什么数组的题目特别多呢,因为很多题目本质就是查找问题,而数组是查找的最佳载体。很多复杂的算法都是为了提高查找效率的,例如二分查找、二叉树、红黑树、B+树、Hash和堆等等。另一方面很多算法问题本质上都是查找问题,例如滑动窗口问题、回溯问题、动态规划问题等等都是在寻找那个目标结果。

这里只写最简单的方式,根据值是否相等进行线性查找,基本实现如下:

  1. 返回指定元素首次出现的下标位置
/**
* 顺序查找
* 返回指定元素首次出现的下标位置
*/
public static int sequentialSearch01(int[] arr,int value){
    for (int i = 0; i < arr.length; i++) {
        if(arr[i] == value){
            return i;
        }
    }
    return -1;
}
  1. 返回指定元素出现的下标位置的集合
/**
 * 顺序查找
 * 返回指定元素出现的下标位置的集合
 */
public static List<Integer> sequentialSearch02(int[] arr,int value){
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < arr.length; i++) {
                if(arr[i] == value){
                        list.adds(i);
                }
        }
        return list;
}

3.2 增加一个元素

  1. 在这个修改后的算法中,我们首先检查 size 是否大于或等于原数组 arr 的长度。如果是,则返回 -1 表示操作失败。

  2. 否则,我们需要找到要插入的位置 index,即比要插入的元素 element 大的第一个元素所在的位置。我们通过遍历原数组来寻找该位置。

  3. 然后,我们将 index 之后的元素往后移动一位,为要插入的元素腾出空间。

  4. 最后,我们将要插入的元素 element 放入 index 的位置,并返回 index 作为添加元素后的下标位置。

/**
 * 根据元素大小顺序增加元素到数组中
 *
 * @param arr 原数组
 * @param size 原数组长度
 * @param element 要添加的元素
 * @return 添加元素后的下标位置,如果操作失败则返回 -1
 */
public static int addByElementSequence(int[] arr, int size, int element) {
    if (size >= arr.length){
        return -1;
    }

    int index = size;
    for (int i=0; i<size; i++){
        if (element < arr[i]){
            index = i;
            break;
        }
    }

    for (int j = size; j > index; j--){
        arr[j] = arr[j - 1];
    }
    arr[index] = element;
    return index;
}

当插入元素时,需要考虑边界情况。在给定的代码中,有两个边界需要特别注意:

  1. 达到数组的最大长度:如果 size 大于或等于原数组 arr 的长度,表示数组已经达到了最大容量,无法再添加新的元素。此时,我们返回 -1 表示操作失败。
  2. 插入位置的选择:在确定要插入的位置 index 时,我们遍历原数组,并比较每个元素与要插入的元素的大小。如果找到第一个比要插入元素大的元素,则将 index 设置为该元素的位置。这样,我们可以确保插入元素后的数组仍然保持升序排序。

请注意:

  1. 如果原数组中的所有元素都小于要插入的元素,则 index 的值将保持为 size,表示要插入的元素应该放在数组的末尾。

  2. 在进行数组元素的移动时,我们从尾部开始向前遍历数组,并将每个元素移动到下一个位置。这样做可以确保不会覆盖之前的元素,同时为要插入的元素腾出空间。最后,我们将要插入的元素放置在正确的位置 index 上。

然后将上面的代码进行优化一下,可以边遍历边识别,这样效率更高

public class ArrayUtils {

    /**
     * 根据元素大小顺序增加元素到数组中
     *
     * @param arr 原数组
     * @param size 原数组长度
     * @param element 要添加的元素
     * @return 添加元素后的下标位置,如果操作失败则返回 -1
     */
    public static int addByElementSequence(int[] arr, int size, int element) {
        if (size >= arr.length) {
            return -1;
        }

        int index = size;
        while (index > 0 && element < arr[index - 1]) {
            arr[index] = arr[index - 1];
            index--;
        }
        arr[index] = element;

        return index;
    }
}

在这个修改后的算法中,

  1. 我们从 size 的位置开始向前遍历数组。

  2. 如果要插入的元素 element 小于当前遍历的元素,则将当前元素往后移动一位,并继续向前遍历。这样,我们可以在找到插入位置之前就完成元素的移动和对比查找。

  3. 最后,我们将要插入的元素 element 放入正确的位置 index 上,并返回 index 作为添加元素后的下标位置。

通过从后往前的遍历和移动方式,我们确实可以减少一次对数组的遍历,进一步提高了算法的效率。

3.3 删除一个元素

对于删除,就不能一边从后向前移动一边查找了,因为元素可能不存在。所以要分为两个步骤,先查是否存在元素,存在再删除

这个方法电荷增加元素一样,必须自己亲自写才有作用,该方法同样要求删除序列最前、中间、最后和不存在的元素都能有效。

public class ArrayUtils {

    /**
     * 通过元素值删除数组中的元素
     *
     * @param arr 原数组
     * @param size 原数组长度
     * @param key 要删除的元素值
     * @return 删除元素后的数组长度
     */
    public static int removeByElement(int[] arr, int size, int key) {
        int index = -1;
        for (int i = 0; i < size; i++) {
            if (arr[i] == key) {
                index = i;
                break;
            }
        }
        if (index != -1) {
            for (int i = index + 1; i < size; i++) {
                arr[i - 1] = arr[i];
            }
            size--;
        }
        return size;
    }
}
  1. 首先遍历原数组,查找要删除的元素 key 的索引位置 index
  2. 如果找到了要删除的元素,则将该元素之后的所有元素依次向前移动一位,覆盖要删除的元素。
  3. 然后,更新数组的长度 size,使其减少1。

需要注意的是,如果要删除的元素在数组中有多个相同的值,这个方法只会删除第一个出现的匹配项。如果需要删除多个匹配项,可以使用循环来重复调用这个方法。

4. 练习:Leetcode896-单调数组

Leetcode896-单调数组

这题要理解什么是单调递增,什么是单调递减,还有[1,1,1,1,1]这种是即属于单调递增,也属于单调递减

方案一:判断两次

class Solution {
    public static boolean isMonotonic(int[] nums) {
        return isSorted(nums, true) || isSorted(nums, false);
    }

    public static boolean isSorted(int[] nums, boolean increasing) {
        int n = nums.length;
        for (int i = 0; i < n - 1; ++i) {
            if (increasing) {
                if (nums[i] > nums[i + 1]) {
                    return false;
                }
            } else {
                if (nums[i] < nums[i + 1]) {
                    return false;
                }
            }
        }
        return true;
    }
}
  1. 首先假设为单调递增,然后去判断,如果是真,则返回true,否则为false

  2. 再次假设判断是否为单调递减,如果是真,则返回true,否则为false

  3. 那么就会有四种可能:true | true = true(如果数组是这种[1,1,1,1]), true | false = true, false | true = true, false | false = false

  4. 所以当既不是递增和递减,就是false,所以符合题目的解法

复杂度如下:

  1. 时间复杂度:在 isMonotonic 方法中,调用了两次 isSorted 方法。而 isSorted 方法的时间复杂度是 O(n),其中 n 是数组的长度。因此,整个算法的时间复杂度是 O(n)。
  2. 空间复杂度:算法只使用了常量级别的额外空间,存储了一些变量和参数,不随输入规模变化。因此,算法的空间复杂度是 O(1)。

方案二:一次遍历

public class ArrayUtils {

    /**
     * 判断数组是否为单调递增或单调递减的
     *
     * @param nums 给定的整型数组
     * @return 如果数组为单调递增或单调递减的,返回 true;否则返回 false
     */
    public static boolean isMonotonic_2(int[] nums) {
        boolean inc = true, dec = true;
        int n = nums.length;
        for (int i = 0; i < n - 1; ++i) {
            if (nums[i] > nums[i + 1]) {
                inc = false;
            }
            if (nums[i] < nums[i + 1]) {
                dec = false;
            }
        }
        return inc || dec;
    }
}

时间复杂度仍然是 O(n),因为我们需要遍历整个数组。空间复杂度是 O(1),因为只使用了常量级别的额外空间。

5. 练习:Leetcode88-合并两个有序数组

88. 合并两个有序数组

方法1:先合并再排序实现排序

/**
 * 方法1:先合并再排序实现排序
 *
 * @param nums1 第一个数组
 * @param nums1_len 第一个数组的长度
 * @param nums2  第二个数组,将nums2合并到nums1中
 * @param nums2_len 第二个数组的长度
 */
public static void merge1(int[] nums1, int m, int[] nums2, int n) {
    for (int i = 0; i < m; ++i) {
        nums1[m + i] = nums2[i];
    }
    Arrays.sort(nums1);
}

空间复杂度为O(1),因为它只使用了常数级别的额外空间来存储变量和索引。

时间复杂度约为O(nums2_len + (nums1_len + nums2_len)log(nums1_len + nums2_len))。

方法2:两个数组从后向前逐步合并

首先,我们定义三个指针:ilen1len2。其中,i指向nums1的末尾,len1指向nums1中的最后一个元素,len2指向nums2中的最后一个元素。

然后,我们使用一个循环来比较nums1nums2的元素,并将较大的元素放入nums1的末尾。具体的比较逻辑如下:

  • 如果nums1[len1] <= nums2[len2],表示nums2中的当前元素较大或两个元素相等。此时,将nums2[len2]放入nums1[i],然后ilen2都向前移动一位。
  • 如果nums1[len1] > nums2[len2],表示nums1中的当前元素较大。此时,将nums1[len1]放入nums1[i],然后ilen1都向前移动一位。

上述步骤会一直重复,直到其中一个数组的所有元素都被遍历完毕。

如果存在某个数组还有剩余元素,意味着剩下的元素都比已经合并的部分的元素要小。因此,我们只需将剩余的元素按照顺序放入nums1的前面即可。

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int i = m + n - 1;
        int len1 = m - 1, len2 = n - 1;
        while (len1 >= 0 && len2 >= 0) {
            if (nums1[len1] <= nums2[len2])
                nums1[i--] = nums2[len2--];
            else if (nums1[len1] > nums2[len2])
                nums1[i--] = nums1[len1--];
        }
        //假如A或者B数组还有剩余
        while (len2 != -1) nums1[i--] = nums2[len2--];
        while (len1 != -1) nums1[i--] = nums1[len1--];
    }
}

空间复杂度为O(1),与之前的优化方法相同。

时间复杂度为O(nums1_len + nums2_len)

方法3:微调优化方法2

这里的优化只是一个微调,当其中一个数组已经被遍历完毕后,我们可以提前结束循环,而不需要再继续比较剩下的元素。这样可以减少一些不必要的比较操作。

另外,在将剩余的元素放入nums1之前,我们可以先检查一下nums1nums2中剩余元素的情况。如果nums1中还有未处理的元素,那么它们已经在正确的位置上,不需要再移动。只需要将nums2中剩下的元素按顺序放入nums1的前面即可。同样地,如果nums2中还有未处理的元素,将它们直接放到nums1的前面即可。

public static void merge(int[] nums1, int m, int[] nums2, int n) {
    int i = m + n - 1;
    int len1 = m - 1, len2 = n - 1;

    while (len1 >= 0 && len2 >= 0) {
        if (nums1[len1] <= nums2[len2]) {
            nums1[i--] = nums2[len2--];
        } else {
            nums1[i--] = nums1[len1--];
        }
    }

    while (len2 >= 0) {
        nums1[i--] = nums2[len2--];
    }
}

这个优化后的方法可以更早地结束循环,并且对剩余元素的处理更加简洁和高效。

它仍然具有O(nums1_len + nums2_len)的时间复杂度和O(1)的空间复杂度。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值