算法学习——数组与字符串解题技巧(一)

767 阅读7分钟

这是我参与更文挑战的第6天,活动详情查看:更文挑战

1、数组与字符串简介

1.1 一维数组

  一维数组,是大家都很熟知的一种数据结构。数组中的元素在内存中是连续存储的,每个元素在数组中都有一个索引值(数组下标),通过索引,可以实现快速访问数组中的元素。

数组4种常见的操作

  • 通过索引值读取元素,时间复杂度为O(1)
  • 查找特定元素,时间复杂度为O(n)
  • 插入元素,时间复杂度为O(n)
  • 删除元素,时间复杂度为O(n)

1.2 二维数组

  二维数组是一种结构较为特殊的数组,只是将数组中的每个元素变成了一维数组。类似一维数组,对于一个二维数组 A = [[1, 2, 3, 4],[2, 4, 5, 6],[1, 4, 6, 8]],计算机同样会在内存中申请一段连续的空间,并记录第一行数组的索引位置,即 A[0][0] 的内存地址。

1.3 字符串

维基百科:字符串是由零个或多个字符组成的有限序列。一般记为 s = a1a2...an。它是编程语言中表示文本的数据类型。

  字符串与数组有很多相似之处,比如使用**名称[下标]**来得到一个字符,比如Java的String对象底层就是使用数组方式实现。字符串作为编程中最为常见且重要的数据类型之一,对其进行研究也是非常重要的。字符串操作比其他数据类型更复杂,常见的操作如翻转字符串、编辑距离、字符串匹配算法...这些都是需要花功夫去研究才能写好。

2、双指针技巧

  通过迭代数组解决问题,是常规操作之一,通常,我们只需要一个指针进行迭代,即从数组中的第一个元素开始,最后一个元素结束。然而,有时我们会使用两个指针进行迭代。

单指针与双指针.png

2.1 头尾指针

  头尾指针,顾名思意,就是有两个指针分别指向数组的开头和结尾。有些题目可以一眼看出,使用头尾指针的技巧,可以快速解题,如反转字符串。我们熟知的快排,也有使用头尾指针的技巧。

2.1.1 题型解析:两数之和 II - 输入有序数组

给定一个已按照升序排列的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。

函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length 。

你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。

示例:

  • 输入:numbers = [2,7,11,15], target = 9
  • 输出:[1,2]
  • 解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。

  因为给定数组已有序,首先可以考虑到使用二分法,二分法对于有序数组查找效率为O(log n)。先固定第一个数,然后用二分法查找另一个数使两数之和等于target。如果找到则返回,找不到则固定第二个数,继续查找,如下。

for i to n
    if(binarySearch(i+1, n, target - num[i], int[] num){
        return;
    }       

  使用二分查找的方法,尽管二分查找效率很快,但需要从头到尾固定一个数,总体时间复杂度为O(n * log n)。

  使用头尾指针的方法,初始时两个指针分别指向第一个元素位置和最后一个元素的位置。因为数组时有有序的,可以得到一个明确的指针移动方向。

  • 每次计算两个指针指向的两个元素之和,并和目标值比较。
  • 如果两个元素之和等于目标值,则发现了唯一解。
  • 如果两个元素之和小于目标值,则将左侧指针右移一位。
  • 如果两个元素之和大于目标值,则将右侧指针左移一位。
  • 移动指针之后,重复上述操作,直到找到答案。

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组的长度。两个指针移动的总次数最多为 n 次。
  • 空间复杂度:O(1)。
class Solution {
    public int[] twoSum(int[] numbers, int target) {
    	int[] result = new int[2];
    	int minIndex = 0;
    	int maxIndex = numbers.length-1;
    	while(minIndex != maxIndex){
    		if(numbers[minIndex] + numbers[maxIndex] == target){
    			result[0] = minIndex+1;
    	    	result[1] = maxIndex+1;
    	    	return result;
    		}
    		if(numbers[minIndex] + numbers[maxIndex] < target){
    			minIndex ++;
    		}
    		if(numbers[minIndex] + numbers[maxIndex] > target){
    			maxIndex --;
    		}
    	}
    	return result;        
    }
}

头尾指针练习题有序数组的平方

2.2 快慢指针

  头尾指针通过头尾指针相向而行,达到遍历数组的目的。而快慢指针,则是使用两个不同步的指针来解决问题。与头尾指针不同的是,快慢指针移动方向是相同的。经典题型如移除元素

2.2.2 题型解析移除元素

给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

  题目中要求,原地移除所有数值等于val的元素。则操作上,每次移除一个元素,都需要将后面的元素向前移动。暴力解法就是,两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。这样的解法复杂度为O(n^2)。**如果知道每个元素之前有多少个值为val的元素,每个元素就可以一步到位移动到需要的位置上。**使用快慢指针可以做到。

  • 快指针指向将要操作的元素,慢指针指向下一个将要赋值的位置。
  • 如果快指针指向的元素值为val,则快指针右移一位,慢指针不动。
  • 如果快指针指向的元素值不为val,则将快指针指向的元素赋值为慢指针,快慢指针都右移一位。
  • 快指针完成数组遍历时,完成移除元素操作,慢指针的值就是输出数组的长度。
class Solution {
    public int removeElement(int[] nums, int val) {
    	int result = 0;
        for(int i = 0;i<nums.length;i++){
        	if(nums[i] != val){
        		nums[result] = nums[i];
        		result++;
        	}
        }
    	return result;        
    }
}

题外话:原题中,不要删除之后数组保持原有的顺序,也可以使用头尾指针完成移除元素。

3、滑动窗口

  滑动窗口,可以看作是双指针技巧的一种变体。所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。因为不断调节子序列的起始位置(起始指针)和终止位置(终止指针),看起来就像一个窗口在数组中移动,所以称为滑动窗口。

滑动窗口题型,主要注意以下三点:

  • 窗口内是什么
  • 如何移动窗口的起始位置
  • 如何移动窗口的终止位置

通过一个题:长度最小的子数组,来具体讲讲。

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

  • 示例:
  • 输入:s = 7, nums = [2,3,1,2,4,3]
  • 输出:2
  • 解释:子数组 [4,3] 是该条件下的长度最小的子数组。

滑动窗口.png

回到刚刚说的需要注意的三个点

  • 本题中,窗口就是满足其和 ≥ s 的长度最小的连续子数组
  • 窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
  • 窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,窗口的起始位置设置为数组的起始位置就可以了。
class Solution {
    public int minSubArrayLen(int s, int[] nums) {
        int n = nums.length;
        if (n == 0) {
            return 0;
        }
        int ans = Integer.MAX_VALUE;
        int start = 0, end = 0;
        int sum = 0;
        while (end < n) {
            sum += nums[end];
            while (sum >= s) {
                ans = Math.min(ans, end - start + 1);
                sum -= nums[start];
                start++;
            }
            end++;
        }
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }
}