java小白学数组——第三关(青铜挑战)

开始:

一般人学习数据结构或者算法都是从数组开始,但是这里并没有从数组开始,主要有以下几点原因:

  • 数组可以作为高级算法的载体,相关题目特别特别多,类型非常多,难度差异也很大,刷的时候成就感比较低。
  • 数组算法需要花很多精力处理边界问题,经常是算法写出来了,执行就是不对,非常让人恼火。
学不完的数组
内容1.理解数组存储元素的特征
2.实现在数组的首部、中间和尾部插入元素
3实现在数组的首部、中部和尾部删除元素
4.实现单调数组算法
5.实现数组合并问题

 1.线性表基础

1.1线性表

我们先要搞清楚几个基本概念,在很多地方都会看到线性结构、线性表这样的表述,那什么是线性结构?与数组、链表等有什么关系?常见的线性结构又有哪些呢?

所谓线性表就是具有相同特征元素的一个有限序列,其中所含元素的个数称为线性表的长度,从不同的角度看,线性表也有不同的分类,例如:

从语言实现的角度

顺序表有两种基本实现方法,一体式和分离式,如下图:

图a为一体式结构,存储表信息的单元与元素存储区域以连续的方式安排在一块存储区里,两部分数据的整体形成一个完整的顺序表对象。这种结构整体性强,易于管理。但是由于数据元素存储区域是表对象的一部分,顺序表创建后,元素存储区就固定了。C和C++都是一体式的结构。 

图b为分离式结构,表对象只保存与整个表有关的信息(即容量和元素个数),实际数据元素存放在另一个独立的元素存储区里,通过链接与基本表对象关联。Java和python是分离式结构。

从存储的角度 

 可以分为顺序型和链表型。

顺序型就是将数据存放在一段固定的区间内,此时访问元素的效率非常高,但是删除和增加元素代价比较大,如果要扩容只能整体搬迁。

链表型就是元素之间是通过地址依次连接的,因此访问时必须从头开始逐步向后找,因此查找效率非常低,但是删除和增加元素非常方便,并且不需要考虑扩容的问题。链表的常见实现方式又有单链表、循环链表、双向链表等等。

从访问限制的角度

栈和队列又称为访问受限的线性表,插入和删除受到了限制,只能在固定的位置进行。而Hash比较特殊,其内部真正存储数据一般是数组,但是访问是通过映射来实现的,因此大部分材料里并不将Hash归结到线性表中,这里为了使学习更加紧凑,将其与队栈一起学习。

线性的知识框架如下:

从扩容的角度 

采用分离式结构的顺序表,若将数据区更换为存储空间更大的区域,则可以在不改变对象的前提下对其数据区进行扩充,所有使用这个表的地方都不必修改,只要程序的运行环境(计算机系统)还有空闲存储,这种表结构就不会因为满了而导致操作无法进行。人们把采用这种技术实现的顺序表称为动态顺序表,因为其容量可以在使用中动态变化。

扩充的两种策略:

  • 第一种:每次扩充增加固定数目的存储位置,如每次扩充增加10个元素位置,这种策略称为线性增。特点:节省空间,但是扩充操作频繁,操作次数多。
  • 第二种:每次扩容量加倍,如每扩充增加一倍存储空间。特点:减少了扩充操作的执行次数。但是可能会浪费空间资源。以空间换时间,推荐方式。

具体到每种结构语言中的结构,实现方式千差万别。其中Java基本是扩容时加倍的方式,而在python的官方实现中,list采用了如下的策略:在建立空表(或者很小的表)时,系统分配一块能容纳8个元素的存储区;在执行插入操作(insert或者append)时,如果元素存储区满就换一块4倍大的存储区。但如果此时的表已经很大(目前的阀值为50000),则改变策略,采用加1倍的方法。引入这种改变策略的方式,是为了避免出现过多的空闲存储位置。 

1.2数组的概念

数组是线性表最基本的结构,特点是元素是一个紧密在一起的序列,相互之间不需要记录彼此的关系就能访问的,例如月份,星座等等。

数组用索引的数字来标识每项数据在数组中的位置,且在大多数编程语言中,索引是从0开始算起的。我们可以根据数组中的索引快速访问数组中元素。

 数组有两个需要注意的点,一个是从0开始记录,也就是第一个村元素的位置是a[0],最后一个是a[length-1]。其次,数组中元素在内存中是连续存储的,且每个元素占用相同大小的内存。

另外需要注意的是数组空间不一定是满的,100的空间可能只能用10个位置,所以需要注意数据个数的变量size和数组长度的length可能不一样。解题的时候需要注意。 

1.3数组存储元素的特征

 第一问题:我创建了一个大小为10的数组,请问此时的数组里面是什么?

回答:不同的语言处理会不一样,在c语言里每一个位置都是一个随机数。而在java里,默认会初始化为0。而python更为灵活可以直接指定是什么,例如a=[1,2,3,4],就是数组里面有四个元素,而a=[0 for i in range(10)]这样定义的数组就是[0,0,0,0,0,0,0,0,0 ]

第二个问题:是否可以初始化一部分位置?初始化的本质是什么?

回答:当然可以,可以将前面五个位置依次,后面的空着,此时的数组内容为{1,2,3,4,5,0,0,0,0,0}

初始化的本质就是覆盖已有的值,用你需要的值覆盖原来的0,因为数组本来是{0,0,0,0,0,0,0,0,0 },这里只不过被你替换成了{1,2,3,4,5,0,0,0,0,0}。如果此时你想知道有效元素的个数,就必须在使用一个额外的变量,例如size来标记。

第三个问题:上面已经初始化的元素之间是否可以空着,例如初始化为{1,0,0,4,5,0,2,0,3,0}。其中0位置仍然是初始化的?

回答:不可以!绝对不可以!要初始化,就必须从前向后的连续空间初始化,不可以出现空缺的情况,这是违背数组的原则的。你正在进行某种运算期待可以先给部分位置赋值,而一旦稳定了,就不可以再出现空位置的情况。

第四个问题:如果我需要的数据就是在中间的某一段该怎么办呢?例如{0,0,3,4,5,6,7,0,0,0},此时该怎么拿到3到7的元素呢?

回答:你需要使用两个变量,如果left=2,right=6来表示区间[left,right]是有效的。

第五个问题:我删除的时候,已经被删除的位置该是什么呢?例如原始数组为{1,2,3,4,5,6,7,8,0,0},我删除4之后,根据数组的移动原则,从5开始向前移动,变成{1,2,3,5,6,7,8,?,0,0},那原来8所在的位置应该是什么呢?

答案: 仍然是8,也就是删除4之后的结构为{1,2,3,5,6,7,8,8,0,0},此时表示元素数量的size会减1变成7,原来8的位置仍然是8。因为我们是通过size来标记元素的数量的,所以最后一个8不回被访问到。

且不能处理掉最后一个8

2.数组基本操作

2.1数组创建和初始化

int [] arr = new int [10];

初始化数组最基本的方式是循环赋值:

for(int i=0;i<arr.length;i++)
arr[i]=i;

但是这种方式在面试题一般不行,因为很多题目会给定若干测试数组让你都能测试通过,例如给你两个数组[0,1,2,3,4,5,6,7]。不用循环可以这样写

int [] arr = new int []{0,1,2,3,4,5,6,7}
//或者这么写
int [] sums = {0,1,2,3,4,5,6,7}

2.2查找一个元素

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

下面代码是根据值是否相等进行线性查找

//size 已经存放的元素个数
//key代查找的元素
public static int findByElement(int[] arr,int size,int key){
        for(int i=0;i<size;i++){
            if(arr[I] == key){
                return i;
    }
return -1;
}

2.3增加一个元素

将给定的元素插入到有序数组的对应位置中,我们可以先找位置,再将其后元素整体右移动,最后插入到空位置上。

这里需要注意,算法必须能保证在数组的首部、尾部、和中间位置插入都可以成功。

/**
     * @param arr
     * @param size    数组已经存储的元素数量
     * @param element 待插入的元素元素
     * @return
     */
    public static int addByElementSequence(int[] arr, int size, int element) {
        //问题一:是否应该是size>>.length 
        if (size >= arr.length)
            return -1;
        //问题二想一想这里是否是index=0或者size-1?
        int index = size;
        //找到新元素的插入位置,问题三:这里应该是size-1?
        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]; //index下标开始的元素后移一个位置
        }
        arr[index] = element;//插入数据
        return index;
    }

问题一:这里注意size 是从1开始编号的,表示的就是实际元素的个数。而arr.length也是从1开始的,当空间满的时候就是size=arr.length,此时就不能再插入元素了。

问题二:只能index =size,0或者size-1都不对,例如已有序列为{3,4,7,8},如果插入的元素比8大,例如9,假如index=0 ,则最后结果是{9,3,4,7,8}。假如index=size-1,最后结果就是{3,4,7,9,8}

2.4删除一个元素

对于删除,不能一边从后向前移动一边查找了,因为元素可能不存在。

所以要分为两个步骤,先从最左侧开始查是否存在元素,如果元素存在,则从该位置开始执行删除操作。

例如序列是1 2 3 4 5 6 7 8 9,要删除5,则应该先遍历,找到5,然后从5开始执行删除操作,也就是从6开始逐步覆盖上一个元素,最终序列变成1 2 3 4 6 7 8 9 [9]。

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

/**
     * 遍历数组,如果发现目标元素,则将其删除,
     * 数组的删除就是从目标元素开始,用后续元素依次覆盖前继元素
     *
     * @param arr  数组
     * @param size 数组中的元素个数
     * @param key  要删除的目标值
     */
    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;
    }

3.算法热身——单调数组问题

LeetCode896题目,判断一个给定的数组是否为单调数组。https://leetcode.cn/problems/monotonic-array/description/

方法一:

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; // 如果不是单调递增,则返回false
            }
        }else{
            // 如果要求单调递减,检查当前元素是否小于后一个元素
            if (nums[i] < nums[i + 1]) {
                return false; // 如果不是单调递减,则返回false
            }
        }
    }
    // 如果数组是单调的,返回true
    return true;
}

方法二:

public boolean isMonotonic(int[] nums) {
    int n = nums.length; // 获取数组长度
    boolean increase = true; // 假设数组是单调递增的
    int index = 0; // 初始化索引

    // 找到第一个突破单调性的位置
    while(index < n - 1){
        if(nums[index] < nums[index + 1]){
            break; // 如果后一个元素大于前一个元素,说明数组是单调递增的
        } else if (nums[index] > nums[index + 1]){
            increase = false; // 如果后一个元素小于前一个元素,说明数组是单调递减的
            break;
        } else {
            index++; // 如果相邻元素相等,则继续往后找
        }
    }

    // 根据单调性检查数组
    for(int i = index; i < n - 1; i++){
        if(increase){
            if(nums[i] > nums[i + 1]){
                return false; // 如果要求单调递增,但是出现递减,则返回false
            }
        }else{
            if(nums[i] < nums[i + 1]){
                return false; // 如果要求单调递减,但是出现递增,则返回false
            }
        } 
    }

    return true; // 如果数组是单调的,返回true
}

 我们判断整体单体性也不是白写的,很多时候需要将特定元素插入到有序序列中,并保证插入后的序列仍然有序,例如LeetCode35:https://leetcode.cn/problems/search-insert-position/description/

 如果问你,如何更快速的找到目标元素呢?只要是提到在单调序列中查找的情况,应该立马想到是否能用二分提高查找效率,二分查找后面再说吧。代码如下:

public static int searchInsert(int[] nums, int target) {
    int n = nums.length; // 获取数组长度
    int left = 0, right = n - 1, ans = n; // 初始化左右指针和答案

    // 二分查找
    while (left <= right) {
        int mid = ((right - left) >> 1) + left; // 计算中间位置
        if (target <= nums[mid]) {
            ans = mid; // 更新答案为当前位置
            right = mid - 1; // 在左半部分继续查找
        } else {
            left = mid + 1; // 在右半部分继续查找
        }
    }
    return ans; // 返回最终答案
}

4.算法热身——单调数组问题 

先看如何合并两个有序数组,LeetCode88题:https://leetcode.cn/problems/merge-sorted-array/description/​​​​​​​​​​​​​​

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int len1 = m - 1; // 数组 nums1 的有效元素末尾索引
        int len2 = n - 1; // 数组 nums2 的有效元素末尾索引
        int len = m + n - 1; // 合并后数组的末尾索引

        // 从后向前合并两个有序数组
        while(len1 >= 0 && len2 >= 0) {
            // 判断 nums1 和 nums2 当前位置的值大小,较大的放到合并后数组的末尾
            // 注意--符号在后面,表示先进行计算再减1,这种缩写缩短了代码
            nums1[len--] = nums1[len1] > nums2[len2] ? nums1[len1--] : nums2[len2--];
        }

        // 如果 nums2 还有剩余元素,将其拷贝到 nums1 的开头
        // System.arraycopy() 方法用于将一个数组中的某一范围的元素复制到另一个数组的指定位置
        // 表示将 nums2 数组从下标0位置开始,拷贝到 nums1 数组中,从下标0位置开始,长度为 len2+1
        System.arraycopy(nums2, 0, nums1, 0, len2 + 1);
    }
}

 哇!我今天可太累了,至少敲键盘的时候4个小时多没动地方。如果你觉得这个帖子对你学习数组有帮助的话,留下你的点赞和收藏。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值