测试开发基础之算法(2):数组的特点及相关算法

数组是一种线性表结构,用一段连续的内存存储相同数据类型的数据。

时间复杂度:因为在内存中是连续存储的,所以利用寻址公式,它支持时间复杂度为O(1)的随机访问操作,但是插入和删除的操作因为涉及到数据搬移,所以平均情况时间复杂度为 O(n)

在使用数组时,要警惕数组越界,特别在C语言中访问越界的数据也不会报错。但是像Python、Java语言则会做越界检查。

下面通过一些Leetcode上有关数组的操作的题目,来掌握数组相关的算法。

1、合并两个有序整数数组

from typing import List
class Merge:
    """
    给定两个有序整数数组 nums1 和 nums2,将 nums2 合并到 nums1 中,使得 num1 成为一个有序数组。
    nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素
    https://leetcode-cn.com/problems/merge-sorted-array/
    """

    @staticmethod
    def merge_from_head_to_tail(nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        """
        双指针法,从前往后。
        将指针p1 置为 nums1的开头, p2为 nums2的开头,在每一步将最小值放入输出数组中。
        由于 nums1 是用于输出的数组,需要将nums1中的前m个元素放在其他地方,也就需要 O(m)的空间复杂度。
        :param nums1:
        :param m: nums1的有效数据个数
        :param nums2: 将 nums2 合并到 nums1 中
        :param n: nums2的有效数据个数
        :return: None
        """
        nums1_copy = nums1[:m]  # 复制nums1中的有效数据到nums1_copy,空间复杂度:O(m)
        i = 0  # 表示合并前nums1_copy的下标
        j = 0  # 表示合并前nums2的下标
        for k in range(m + n):  # k表示合并后的数组的下标,m是nums1的有效元素个数,n表示nums2的有效元素个数。时间复杂度是O(m+n)
            # 注意:要把 nums1_copy 和 nums2 归并完成的逻辑写在前面,否则会出现数组下标越界异常
            if i == m:  # nums3已经遍历完了
                nums1[k] = nums2[j]  # 将nums2中的剩余元素陆续加到nums1中
                j = j + 1
            elif j == n:  # nums2遍历完了
                nums1[k] = nums1_copy[i]  # 将nums3中的剩余元素陆续加到nums1中
                i = i + 1
            elif nums2[j] > nums1_copy[i]:
                nums1[k] = nums1_copy[i]  # 将num2和num3中较小的数放入到nums1中
                i = i + 1
            else:
                nums1[k] = nums2[j]
                j = j + 1

    @staticmethod
    def merge_from_tail_to_head(nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        """
        双指针 / 从后往前
        上面的方法需要使用额外空间。这是由于在从头改变nums1的值时,需要把nums1中的元素存放在其他位置。
        如果从结尾开始改写 nums1 的值,则不需要额外的空间,因为nums1的尾部是没有数据的。
        :param nums1: nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素
        :param m: nums1的有效数据个数
        :param nums2: 将 nums2 合并到 nums1 中
        :param n: nums2的有效数据个数
        :return: None
        """
        i = m - 1  # nums1的下标
        j = n - 1  # nums2的下标
        for k in range(m + n - 1, -1, -1):  # 时间复杂度是O(M+N),空间复杂度是O(1)
            if i == -1:  # nums1的有效数据遍历完了,将nums2逐个加入到nums1中
                nums1[k] = nums2[j]
                j = j - 1
            elif j == -1:  # nums2遍历完了,那就可以结束了
                break
            elif nums2[j] > nums1[i]:  # 找nums1和nums2中的大者,放入nums1
                nums1[k] = nums2[j]
                j = j - 1
            else:
                nums1[k] = nums1[i]
                i = i - 1

2、旋转数组

class Rotate(object):
    """
    给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
    举例:
        输入: [1,2,3,4,5,6,7] 和 k = 3
        输出: [5,6,7,1,2,3,4]
    """

    @staticmethod
    def rotate_array_by_raw(nums: List[int], k: int) -> None:
        """
        时间复杂度:O(n*k) 。每个元素都被移动 1 步(复杂度是O(n))经过k次循环(复杂度是O(k)),乘法法则时间复杂度是O(n*k);不推荐这种办法。
        空间复杂度是O(1)
        """

        for i in range(k):
            last = nums[-1]
            for j in range(len(nums) - 1, 0, -1):
                nums[j] = nums[j - 1]
            nums[0] = last

    @staticmethod
    def rotate_array_by_join(nums: List[int], k: int) -> None:
        """
        时间复杂度:O(n);空间复杂度:O(1),没有使用额外的空间。
        """
        k = k % len(nums)
        nums[:] = nums[-k:] + nums[:-k]

    @staticmethod
    def rotate_array_by_rotate(nums: List[int], k: int) -> None:
        """
        翻转三次。
        时间复杂度:O(n);空间复杂度:O(1),没有使用额外的空间。
        """
        k = k % len(nums)
        nums[:] = nums[::-1]
        nums[:k] = nums[:k][::-1]
        nums[k:] = nums[k:][::-1]

3、寻找中心索引

def find_pivot_index(nums: List[int]) -> int:
    """
    请编写一个能够返回数组“中心索引”的方法。
    我们是这样定义数组中心索引的:数组中心索引的左侧所有元素相加的和等于右侧所有元素相加的和。
    如果数组不存在中心索引,那么我们应该返回 -1。如果数组有多个中心索引,那么我们应该返回最靠近左边的那一个。
    :param nums:整型数组
    :return:中心索引
    举例:
        输入: nums = [1, 7, 3, 6, 5, 6]
        输出: 3
    时间复杂度:O(N),其中 N 是 nums 的长度。
    空间复杂度:O(1),使用了 _sum 和 left_of_sum 。
    """
    sum_of_left = 0
    _sum = sum(nums)
    for j in range(len(nums)):  # 用enumerate也可以
        sum_of_right = _sum - sum_of_left - nums[j]
        if sum_of_left == sum_of_right:
            return j
        sum_of_left = sum_of_left + nums[j]
    return -1

4、数组最小元素的最大和

def array_pair_sum(nums: List[int]) -> int:
    """
    给定长度为 2n 的数组, 将这些数分成 n 对, 例如 (a1, b1), (a2, b2), ..., (an, bn) ,使得从1 到 n 的 min(ai, bi) 总和最大。
    举例:
        输入: [1,4,3,2]
        输出: 4
        解释: n 等于 2, 最大总和为 4 = min(1, 2) + min(3, 4)。
    思路:每对组合的两个元素之差最小,将会导致总和最大。可以对数组进行从小到大排序,然后两两组合配对,这样的配对导致每个组合中的差最小,从而导致所需要的总和最大。
    """
    nums.sort()
    _sum = 0
    for i in range(0, len(nums), 2):
        _sum = _sum + nums[i]
    # _sum = sum(nums[0::2])  # 效率更高
    return _sum  # 排序后的奇数位置元素之和。

5、数组形式的整数加法

class Solution:
    """
    对于非负整数 X 而言,X 的数组形式是每位数字按从左到右的顺序形成的数组。例如,如果 X = 1231,那么其数组形式为 [1,2,3,1]。
    给定非负整数 X 的数组形式 nums,返回整数 X+k 的数组形式。
    """

    def add_to_array_form(self, nums: List[int], k: int) -> List[int]:
        """
        先把数组转成字符串,再转成整数,与K做和之后,转成字符串,再将字符串转成数组
        """
        return [int(j) for j in str(int("".join([str(i) for i in nums])) + k)]

6、删除数组中的重复元素

class RemoveDuplicate:
    """
    给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。要求在O(1)的空间复杂度内完成。
    """

    @staticmethod
    def remove_duplicate_1(nums: list) -> int:
        for i in range(len(nums) - 1, 0, -1):
            if nums[i] == nums[i - 1]:
                nums.pop(i)
        return len(nums)  # list(set(nums))

    @staticmethod
    def remove_duplicate_2(nums: list) -> int:
        """
        双指针法,慢指针i,快指针j,都从0开始。当快指针到达尾部时,慢指针i就是不重复数组的最后一个下标。
        只要nums[i]=nums[j],我们就增加 j 以跳过重复项。
        遇到 nums[j]!=nums[i],慢指针加1,将nums[j]赋值给nums[i]
        :param nums:
        :return:
        """
        length = len(nums)
        if length <= 1:
            return length
        i = 0
        for j in range(length):
            if nums[i] != nums[j]:
                i = i + 1
                nums[i] = nums[j]
        return i + 1

7、按奇偶排序数组

class Sort:
    """
    给定一个非负整数数组 nums,返回一个数组,在该数组中, nums 的所有偶数元素之后跟着所有奇数元素。
    举例:
        输入:[3,1,2,4]
        输出:[2,4,3,1]
        输出 [4,2,3,1],[2,4,1,3] 和 [4,2,1,3] 也会被接受。
    """

    @staticmethod
    def sort_array_by_parity_1(nums: List[int]) -> List[int]:
        """
        时间复杂度:O(N),其中 N 是 nums 的长度。空间复杂度:O(N),存储结果的数组。
        """
        return [x for x in nums if x % 2 == 0] + [y for y in nums if y % 2 == 1]

    @staticmethod
    def sort_array_by_parity_2(nums: List[int]) -> List[int]:
        """
        时间复杂度:O(N*logN),其中 N 是 nums 的长度。空间复杂度:排序空间为 O(N),取决于内置的 sort 函数实现。
        """
        nums.sort(key=lambda x: x % 2)
        return nums

    @staticmethod
    def sort_array_by_parity_3(nums: List[int]) -> List[int]:
        """
        如果希望原地排序,可以使用快排,一个经典的算法。
        维护两个指针i和j,分别指向数组的两端。时刻保证i左边的都是偶数(nums[i]%2=0),j右边的都是奇数(nums[j]%2=1)。
        时间复杂度是空间复杂度:排序空间为 O(1)
        """
        i, j = 0, len(nums) - 1
        while i < j:
            if nums[i] % 2 > nums[j] % 2:  # i指向奇数j指向偶数,则交换它们
                nums[i], nums[j] = nums[j], nums[i]
            if nums[i] % 2 == 0:  # i指向偶数,i向右移动1位
                i = i + 1
            if nums[j] % 2 == 1:  # j指向奇数,j向左移动1位
                j = j - 1
        return nums

8、容器能否完全替代数组?

针对数组类型,很多语言都提供了容器类,比如 Java 中的 ArrayList,Python中的List。
ArrayList 或者 List这些容器类,最大的优势就是可以将很多数组操作的细节封装起来。比如数组插入、删除数据时需要搬移其他数据等。
另外,它还有一个优势,就是支持动态扩容,但是扩容的操作是有开销的,如果我们事先能确认数组的大小,最好在创建 ArrayList 的时候事先指定数据大小。
在项目开发中,什么时候适合用数组,什么时候适合用容器呢?对于业务开发,大部分情况下,直接使用容器就足够了,省时省力。但是追求极致性能、非常底层的应用开发时,用数组更合适。

9、参考资料

在这里插入图片描述

发布了187 篇原创文章 · 获赞 270 · 访问量 172万+

猜你喜欢

转载自blog.csdn.net/liuchunming033/article/details/103037171