算法课堂——数组初探

191 阅读9分钟

算法课堂——时空交错

这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」。

在上一节课(算法课堂-- 时空交错)我们学习算法的空间复杂度的时候,提到了许多没有介绍过的数据结构,很多同学对我们算法中常用的数据结构不是很了解,今天我就带大家学习一下算法中比较常见也是最基础的数据结构--数组


一、数据结构

数据结构(data structure)是带有结构特性的数据元素的集合,它研究的是数据的逻辑结构和数据的物理结构以及它们之间的相互关系,并对这种结构定义相适应的运算,设计出相应的算法,并确保经过这些运算以后所得到的新结构仍保持原来的结构类型。简而言之,数据结构是相互之间存在一种或多种特定关系的数据元素的集合,即带“结构”的数据元素的集合。“结构”就是指数据元素之间存在的关系,分为逻辑结构和存储结构。

以上是百度百科对于数据结构的解释。简而言之就是,数据结构是相互之间存在一种或多种特定关系的数据的集合。它可以帮助我们提高算法的执行效率。

相信大家都听说过一个著名公式,“数据结构+算法=程序” 这也恰恰说明算法和数据结构是分不开的,所以说我们想要学好算法,对于各种数据结构的用法以及优缺点也得做到了然于胸。

其实说到现在大家肯定觉得很抽象,其实以后对于接触到的数据结构,大家只要能够从以下三个方面了解数据结构,那么就可以在选择和使用数据结构时做到知其然也知其所以然。

1、数据的逻辑结构:就是说数据元素之间的逻辑关系。是和计算机内部没有关系的一组具有逻辑关系的数据。

2、数据的存储结构:即数据在计算机中存储的形式。

3、数据结构的运算:即数据结构具有的操作,例如,查询,插入,删除,排序等等。

二、数组

那么接下来,就有请我们今天的主角登场,数组。

什么是数组呢?数组可以理解成是将相同类型的若干变量有序地组织在一起的集合,我们常见的数组有,整型数组、字符串数组、对象数组。并且数组里面的数据也可以是数组,即我们的二维数组,以此类推,还有多维数组。

那么我们针对上文提到的理解数据结构的三个方面,解释一下数组这个数据结构。

1、数组里面数据的逻辑结构:很简单,数组里面的数据,类型是一样的,即数组里面的每个元素是同一类型的。

2、数组里面数据的存储结构:我们介绍数组的时候说,数组是有序的组合在一起的集合,这正是数组在计算机中的存储结构,

第1个元素第2个元素。。。。。。第n-1个元素第n个元素

由于我们规定了数组里面每个元素的类型是相同,所以我们数组中每个元素在存储空间中的长度也是一样的。 所以我们只要知道了数组中第一个元素的地址,就能很方便的计算出每个数据的地址。

3、数组的运算:数组最基本的用法用法包括数组的初始化,数组的赋值,数组的遍历,数组的插入,数组的删除,数组的检索,数组的排序、数组的合并等等。。

三、数组相关的题目解析

我们先来一道简单的开胃菜:

找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:

输入: [2, 3, 1, 0, 2, 5, 3] 输出:2 或 3

简单阅读了题目后,大部分同学很快就能有思路,遍历数组,每次把得到的数放到一个容器里,然后每次看下这个容器里面有没有这个数,有的话,就直接返回答案。代码如下:

    public int findRepeatNumber(int[] nums) {
        Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
        for(int i=0;i<nums.length;i++){
           if (hashtable.containsKey(nums[i])) {
            return nums[i];
            }
            hashtable.put(nums[i], i);
        }
        return -1;
    }
 

在这个算法中我们就用到了数组的长度,即nums.length,以及数组的遍历,即我们循环nums.length次,每次取数组的一个元素。

按我们简单分析下我们的算法,时间复杂度和空间复杂度是多少呢:

很明显,我们循环了一次数组,每次执行了一下hashtable.containsKey()方法(时间复杂度O(1)),所以我们算法的时间复杂度是O(n);

由于我们借用了Map<Integer, Integer>来存放了我们数组的值,而且Map的长度和数组的长度是线性相关的,所以我们算法的空间复杂度也是O(n);

那么我们能不能在时间复杂度和空间复杂度上做优化呢?

首先我们想知道有没有重复数,那么我们大概率不能避免遍历一遍数组,那们我们便需要在空间复杂度上寻找突破,要想不用另外的容器存放我们数组的数,我们就必须在数组本身上做文章。通常需要改变数组本本身的方法比较难想到,因为同学们还没有做过很多题,对算法题不是很敏感,没事多练就行。

这里我们思考下,假设一个场景,开学第一天,老师给每个同学安排了座位,每个座位上有对应同学的名字,很快,学生们来了。各自在各自的位子坐了下来,但是小明同学一直站着不动,为什么,因为班级上还有一位叫小明的同学已经坐位子上了,所以说小明的名字和另一个同学重复了,所以没位置了。

好那么我们运用到我们的题目中去,是不是让每个数“坐到”它自己的位置上,当它发现自己位置有“人”了,就可以判断它重复了。代码如下

    public int findRepeatNumber(int[] nums) {
        int temp;
        for(int i=0;i<nums.length;i++){ 
            while (nums[i]!=i){ //如果相等,那么这个数就该在这个位置上,不用管
                if(nums[i]==nums[nums[i]]){
                    return nums[i]; //如果位置被人占了,说明重复了
                }
                //没有重复,让这个数去它该去的位置上。
                temp=nums[i];
                nums[i]=nums[temp];
                nums[temp]=temp;
            }
        }
        return -1;
    }

好了,这样的话,空间复杂度就变成了O(1),也就是没有借用外部的存储空间,实现了数组判重。

好的,趁热打铁,我们再来一题:

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。 示例 1:

输入:nums = [4,1,4,6] 输出:[1,6] 或 [6,1] 示例 2:

输入:nums = [1,2,10,4,1,4,3,3] 输出:[2,10] 或 [10,2]

这题已经给我们规定了时间复杂度和空间复杂度。但是我们先抛开要求,如果乍一看你会想到什么方法,很简单啊,数组里面的数依次取出来放进容器,只要里面有这个数了,那么这个数就不符合要求了,最终得到两个符合要求的数。这个方法很简单,留给同学们当课后习题。(我会在评论区附上我的答案)

那么我们看下空间复杂度O(1)的算法是如何解决这个问题的呢?

首先我们先将问题简化一下:

一个整型数组 nums 里除一个数字之外,其他数字都出现了两次。请写程序找出这一个只出现一次的数字。

这道题比我们的题目简单在,数组只有一个是不同的,所以我们得像一个办法把其他所有的数都去掉,有没有什么想法?划重点,计算机中去掉两个重复数,用异或

如果你能想到异或,那么这个简化问题就再简单不过了,遍历数组,每个值和之前异或出来的结果做异或,最终得到的值就是我们的答案。

    public int[] singleNumber(int[] nums) {
        int x = 0;
        for(int num : nums)  // 1. 遍历 nums 执行异或运算
            x ^= num;
        return x;            // 2. 返回出现一次的数字 x
    }

那么回到原题目,如果有两个不同的数,又该怎么解决呢?

根据我们简化题的思路,我们大可以用一个方法将原数组,分成两个各含一个出现一次的数组。问题是怎么分呢?

这里又要用到计算机知识了:

1、A 和 B 不相等,那么A 和 B 的二进制表达必然是不相同的。

2、与运算特点:A&1 = 1 A的第一位则是1;A&10 = 1 A的第二位是1;A&100 = 1 A的第三位是1,以此类推。。。。

那么,我们先将数组中所有的数进行异或运算,再将这个值与1,10,100,1000。。。进行与运算,就能得到两个不同的数究竟在哪一个位置上不同,那么也就很容易将他们分开。代码如下:

第一步:将数组中所有数做异或。

public int[] singleNumber(int[] nums) {
    int x = 0;
    for(int num : nums) 
        x ^= num;
    return x;            
}

第二步:将结果和1,10,100,1000。。。进行与运算

int a = 1;
while(z & a == 0) {
    a <<= 1
}

最终,遍历数组,将所有的数和我们得到的m进行与运算,就能将数组分成两个各自含有一个只出现一次的数组。

整体代码如下:

    public int[] singleNumbers(int[] nums) {
        int x = 0, y = 0, n = 0, a = 1;
        for(int num : nums)               // 1. 遍历异或
            n ^= num;
        while((n & a) == 0)               // 2. 循环左移,计算 m
            a <<= 1;
        for(int num: nums) {              // 3. 遍历 nums 分组
            if((num & a) != 0) x ^= num;  // 4. 当 num & m != 0
            else y ^= num;                // 4. 当 num & m == 0
        }
        return new int[] {x, y};          // 5. 返回出现一次的数字
    }

到此,这题就解答完毕了。

严格来讲这道题和数组这个数据结构没什么关系了,但是我们可以看到,很多复杂问题都是以数组作为载体来出题的。要解此类问题,不仅需要对数组本身有足够的了解,还需要对计算机的基本知识做到融会贯通。

四、总结

今天关于数组的两道题目,一道简单,一道稍微复杂(知识点偏),虽然大多数同学可以很快的用暴力法解决,但是,想要在时间和空间上做到优化,也是需要动不少脑筋的,希望同学们在做简单的算法题的时候,能去多想,多思考有没有更优解。

五 、预告

下节课,我会给大家介绍数组的好兄弟,链表,这对欢喜冤家在算法中又会擦出怎样的火花呢?且看下节:

算法课堂--铁锁连环(链表)