数据结构与算法之数组

271 阅读4分钟

在计算机科学里,数组数据结构,简称数组(Array),是由相同类型的元素(element)的集合所组成的数据结构,分配一块连续的内存来存储。利用元素的索引可以计算该元素对应的存储地址。

数组如何实现随机访问?

因为数组是由相同类型的元素连续存储的,正是因为这个特性才使得数组能够随机访问。

当计算机为数组分配了一块连续的内存区域后,假设这个内存的区域的起始地址(基地址)称为base_address。我们知道我们只要通过数组下标index指明是那个元素就可以对数组进行访问。其实,当计算机需要访问数组中的某个元素时,它会通过下面的公式寻址,计算出该元素的内存地址:

a[index]_address = base_address + index * data_type_size

index就是我们在访问数组时使用的下标,data_type_size是该数组元素的数据类型所占的字节大小,如int 为4字节。

注意,数组查找的时间复杂度并不是O(1),而是通过下标访问的时间复杂度为O(1)。


低效的“插入”和“删除”

数组为了保存内存数据的连续性,会导致插入、删除操作比较低效。

1. 插入操作

如果我们的数组长度为n,现在如果我们需要将一个数据插入到数组中的第k个位置。为了把第k个位置空出来,我们需要将k~n这部分的元素依次往后挪一位。

显然在第1个位置,需要移动n个元素;第2个位置,需要移动n-1个元素;......;最后一个元素不需要移动元素。

平均需要移动(0+1+2+3+...+n)/ (n+1) ~= O(n)。最好的时间复杂度是O(1),即每次都在最后插入。 最坏的情况是O(n),即在第一个位置,那所有的元素都要移动。

2. 删除操作

删除和插入是类似的,当我们删除第一个元素时,所有元素都要往前挪一位,时间复杂度为O(n)。当我们删除最后一个元素时,不需要挪动任何数据,时间复杂度为O(1)。

在某些特定的场景下,我们并不一定非得追求数据中数据的连续性。我们可以将多次删除操作集中在一起执行。记录已经被删除的元素,直到数组所有元素都被删除,再进行依一次真正的删除,避免数据的多次挪动,提高效率。


防止数组越界

在C语言中,只要不是访问受限的内存,所有的内存空间都是可以自由访问的。若你的这些内存地址下有重要数据,很有可能会被覆盖或更改。因此,当我们用数组下标访问时,要注意避免不要超出数组大小。否则就会越界。


Leetcode刷题

给定两个有序整数数组 nums1 和 nums2,将 nums2 合并到 nums1 中,使得 num1 成为一个有序数组。

说明:

初始化:nums1 和 nums2 的元素数量分别为 m 和 n。 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。 示例:

输入: nums1 = [1,2,3,0,0,0], m = 3 nums2 = [2,5,6], n = 3

输出:[1,2,2,3,5,6]


解法一: 先合并后排序

先合并两个数组然后在排序。

class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        //先合并
        for(int i = m, j = 0; i < (m+n); i++){
            nums1[i] = nums2[j++];
        }
        //后排序
        sort(nums1.begin(), nums1.end());
    }
};

//时间复杂度合并需要O((n+m)*log(n+m))
//空间复杂度为O(1)

解法二:双支针法/从前往后

由于输出的数组为nums1,需要将nums1的前m个元素放到其他地方保存起来,否则会被覆盖。

class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        if(m == 0){
            for(int i = 0; i < n; i++){
                nums1[i] = nums2[i];
            }
            return;
        }
        
        int nums1_copy[m];
        for(int i = 0; i < m; i++){
            nums1_copy[i] = nums1[i];
        }
        
        int p1 = 0, p2 = 0, p = 0;
        while(p1 < m && p2 < n){
            if(nums1_copy[p1] > nums2[p2]){
                nums1[p++] = nums2[p2++];
            }else{
                nums1[p++] = nums1_copy[p1++];
            }
        }
        while(p1 < m){
            nums1[p++] = nums1_copy[p1++];
        }
        
        while(p2 < n){
            nums1[p++] = nums2[p2++];
        }
        
    }
};

//时间复杂度为O(m+n)
//空间复杂度为O(m)

解法三:双支针法/从后往前

从后往前,不会覆盖原有的数据,可以节约空间。

class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {   
        //定义双指针,从后往前
        int p1 = m - 1, p2 = n - 1, index = (m + n) - 1;
        while((p1 >= 0) && (p2 >= 0)){
          if(nums1[p1] < nums2[p2]){
              nums1[index--] = nums2[p2--];
          }else{
              nums1[index--] = nums1[p1--];
          }
        }     
        while(p2 >= 0){
            nums1[index--] = nums2[p2--];
        }
        
    }
};

//时间复杂度O(m+n)
//空间复杂度O(1)