力扣算法之数组

175 阅读7分钟

作者简介

我是阿宝,一名非科班同学,这是我第一篇文章;为什么要选择数据结构与算法开篇呢?

  1. 我在研一阶段,在项目空闲阶段,就会到力扣上刷刷题,接触的比较早;
  2. 理解数据结构有助于理解计算机底层设计和原理,理解算法有助于提升逻辑思维能力;

如何学习?

算法学习还是依托于力扣,一定要多练习;不然,会存在有思路缺写不出来的情况; 然后,就是尽量保持碎片化刷题,没事的时候,奖励自己一道题;

有何收获?

  1. 第一,就是说对常见的算法有了自己的理解,然后,能够写出来;
  2. 第二,在学习算法的过程中,提高了逻辑思维能力;

有什么注意点?

  1. 写之前先想好思路,不要摸棱两可就开始写
  2. 想好思路了,先取几个示例测试一下,看看能不能通过,防止重新再来;

下面,先从数组开始,学习算法;

数组相关算法

数组相关算法,边界问题是一个主要的问题:为了解决这个问题,我感觉最重要的就是要有一个非常清晰的定义。然后,在之后程序的编写中要维护好这个定义。下面以最简单的二分算法为例,说明这个问题:


/**
 * 描述:实现二分查找,找到target所在的索引值
 * 例: [1,2,3,4,5,6,7] target = 5 return 4;
 * 要点:一定要非常清晰的定义好l 和 r 的意义,
 * 在下面的循环中,一定要维护住这个声明
 */
class Solution {
    public int binarySearch(int[] nums, int n, int target) {
        /**
         * 边界的设置与自己的定义相关
         * 我定义的:在[l , r]的范围内找target
         */
        int l = 0;
        int r = n - 1;
        // 当l=r时,[l,r]区间还是有效的
        // 明确了定义以后,很多边界问题就容易处理了
        while (l <= r){
            int mid = (l + r)/2;
            if (nums[mid] == target){
                return mid;
            }
            // 说明target在[l,mid - 1]范围内
            // 这里一定要 - 1 因为我们的定义是闭区间,我们已经知道mid不是目标元素了
            if (target < nums[mid]){
                r = mid - 1;
            }
            // 说明target在[mid + 1,r]的范围内
            if (target > nums[mid]){
                l = mid + 1;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        int[] nums = {1,2};
        Solution solution = new Solution();
        System.out.println(solution.binarySearch(nums, nums.length, 5));

    }
}


关键点就是:

       /**
         * 边界的设置与自己的定义相关
         * 我定义的:在[l , r]的范围内找target
         */
        int l = 0;
        int r = n - 1;

所以,之后的所有程序,都需要维护其定义的状态;我个人是喜欢以[l,r]区间来进行定义的;那么如果转为[l,r)区间可以吗?答案当然是可以的:这个时候程序就如下所示:

/**
 * 描述:实现二分查找,找到target所在的索引值
 * 例: [1,2,3,4,5,6,7] target = 5 return 4;
 * 要点:一定要非常清晰的定义好l 和 r 的意义,
 * 在下面的循环中,一定要维护住这个声明
 */
class Solution {
    public int binarySearch(int[] nums, int n, int target) {
        /**
         * 边界的设置与自己的定义相关
         * 我定义的:在[l , r)的范围内找target
         */
        int l = 0;
        int r = n;
        // 明确了定义以后,很多边界问题就容易处理了
        while (l < r){
            int mid = (l + r)/2;
            if (nums[mid] == target){
                return mid;
            }
            // 说明target在[l,mid)范围内
            // 这里一定要 - 1 因为我们的定义是闭区间,我们已经知道mid不是目标元素了
            if (target < nums[mid]){
                r = mid;
            }
            // 说明target在[mid + 1,r)的范围内
            if (target > nums[mid]){
                l = mid + 1;
            }
        }
        return -1;
    }
}

很显然,根据约定好的定义来编写,那么也可以解决问题。

总结一下:如何写一个正确的程序,

1.一定要明确我们声明的所有变量的含义

2.循环不变量,在循环过程中,不断的维护这些变量的含义

3.如果发生错误的话,取几个小数据集来测试,调试程序需要有耐心

数组相关问题

1.变量定义实例

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
输入:[0,1,0,3,12]
输出:[1,3,12,0,0]

对于这个问题,可以使用暴力解法来解决;先用一个list把非零元素取出来,然后,在把非零元素按顺序写入到数组中,数组中其他的数置为0;时间复杂度为n 空间复杂度为n;时间复杂度太高。

数组问题,很多可以在原数组中,通过索引来进行解决。

设置一个index来表示当前遍历过的所有非零整数,index:代表[0,index)当前遍历的所有的非零整数;

当遍历到非零整数时,把其存在index上即可;

class Solution {
public void moveZeroes(int[] nums) {
        // [0,index)代表当前遍历的所有非零数
        int index = 0;
        for(int i = 0; i < nums.length; i++){
            if (nums[i] != 0 && i != index){
                nums[index] = nums[i];
                index++;
            }else if (nums[i] != 0 && i == index){
                index++;
                continue;
            }

        }
        for(int i = index; i < nums.length; i++){
            nums[i] = 0;
        }
    }
}

此类型的题目还有力扣26,27,80题,其实都是一样的,需要在原数组中,进行操作。

例26:

class Solution {
    public int removeDuplicates(int[] nums) {
        if (nums.length == 0){
            return 0 ;
        }
        // [0,index)中存储符合条件的元素
        int index = 0;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] != nums[i - 1]){
                nums[index] = nums[i - 1];
                index++;
            }
        }
        nums[index] = nums[nums.length - 1];
        return index + 1;
    }

}

综上:对于在一个数组中,进行去重的相关问题,特别是题目提示需要使用O(1)的空间复杂度时,就需要引入index, 在[0,index)中保存符合条件的值。


数组相关算法之双指针、滑动窗口

数组相关算法-下

一、双指针

image.png

先查看一下本题:这个题目给的条件有升序排列,答案唯一。

这个题目有以下,几种解法

1.暴力解法:时间复杂度O(n2);

2.使用其升序的特点,针对nums[i]时,在[i+1,length]中,使用二分查找,这样的时间复杂度就为O(nlogn);

3.使用双指针

4.如果数组不是递增,或者不是两个数,需要找到多个数时,就转化为背包问题,可以使用动态规划来进行实现。

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        int headPointer = 0;
        int endPointer = numbers.length - 1;
        int[] res = new int[2];
        while(headPointer < endPointer){
            if (numbers[headPointer] + numbers[endPointer] == target){
                res[0] = headPointer + 1;
                res[1] = endPointer + 1;
                return res;
            }else if (numbers[headPointer] + numbers[endPointer] > target){
                endPointer--;
            }else{
                headPointer++;
            }
        }
        return res;
    }
}

和双指针相关问题在力扣中还有如下题目 125

image.png

344 反转字符串(所有的反转相关问题)

image.png

345 反转字符串中的元音字母

image.png

11 题盛水最大的容器

image.png

height[i] > height[j]时,j向右移动;

height[i] <= height[j]时,i向左移动;

二、滑动窗口

使用两个索引来代表一个窗口;通过这个窗口的滑动,在这个数组中游走来找到我们需要的解。

image.png

思路如下:

1.暴力解法:先选取nums[0]为起始点,算出和大于target时,它的长度。依次循环,解出最小的那个长度。时间复杂度为O(n2)

2.使用滑动窗口

  1. 设滑动窗口为[l, r];当[l, r]里面的元素大于等于s时,纪录长度;并且将l右移一位;
  2. 当[l, r]里面的元素小于s时,r++;

这样和暴力解法相比就是解决了重复计算的问题。

class Solution {
    public int minSubArrayLen(int s, int[] nums) {
        int length = nums.length;
        // 定义滑动窗口为[l,r]
        int l = 0;
        int r = -1;
        int sum = 0;
        // 假设一个最大值,永远都不可能取到
        int res = length + 1;
        while (l < length){
            if (sum < s){
                r++;
                // 如果[l,r]中值小于s,并且r >= length 循环结束
                if (r >= length){
                    break;
                }
                sum = sum + nums[r];
                
            }else{
                res = Math.min((r - l + 1), res);
                sum = sum - nums[l];
                l++;
            }
        }
        if (res == length + 1){
            return 0;
        }else{
            return res;
        }

    }
}

这样时间负责度为O(n) 空间复杂度为1;

相同的题目还有力扣第3题:

image.png

也是使用滑动窗口的思想;

比如说 s = "abcdcabce";

设置滑动窗口为[l,r];比如说,这里找到abcd都是可以的,当找到c时,发现有重复的元素了,就需要把l移动到c所在的位置上。

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int length = s.length();
        char[] chars = s.toCharArray();
        int res = 0;
        // 定义滑动窗口为[l,r)
        int l = 0;
        int r = 0;
        Set<Character> set = new HashSet<>();
        while (l < length){
            // 如果set中没有该数的话,进行加入
            if (!set.contains(chars[r])){
                set.add(chars[r]);
                r++;
                res = Math.max((r - l), res);
                if (r >= length){
                    break;
                }
            }else{
                    while(chars[l] != chars[r]){
                        set.remove(chars[l]);
                        l++;
                    }
                    set.remove(chars[l]);
                    l++;
            }
        }
        return res;
    }
}

相同的问题还有438题,76题,42题:

image.png

image.png

image.png

对于,76题来说:其实最难的就是在r移动了一下以后,如何确定是否满足了t中子串的条件。主要困难就是这里。

class Solution {
    // 问题:如何从字符串中找到目标子串
    public String minWindow(String s, String t) {
        /**
         * 思路:先新建两个数组int[128],一个count 建立滑动窗口[l,r]
         * 一个存t中的字符,另一个存s中的字符
         * 在存s字符的过程中,如果发现是t中必要的字符,就进行count++
         * 如果count = tLength 就说明找到一个一个目标值; 就让l++ 直到加到不满足条件即可
         * 这时候,就把一个最小的[l,r]区间找到了。把其放入到map中
         */

        int[] tInts = new int[128];
        int[] sInts = new int[128];
        // 滑动窗口为[l, r]
        int l = 0;
        int r = -1;
        // key:存储长度,value:存储l
        HashMap<Integer, Integer> hashMap = new HashMap<>();
        int tLength = t.length();
        int sLength = s.length();
        char[] tChars = t.toCharArray();
        char[] sChars = s.toCharArray();
        int count = 0;

        for (int i = 0; i < tLength; i++) {
            tInts[tChars[i]]++;
        }

        for (int i = 0; i < sLength; i++) {
            char ch = s.charAt(i);
            // 说明这个ch是很必要的,count需要+1
            if (tInts[ch] > 0 && sInts[ch] < tInts[ch]){
                count++;
            }
            sInts[ch]++;

            // 如果他们相等了
            while(count == tLength){
                char leftChar = s.charAt(l);
                // 如果l++ 没有影响的,就l++
                if (tInts[leftChar] == 0){
                    l++;
                    sInts[leftChar]--;
                }else if (tInts[leftChar] > 0 && sInts[leftChar] > tInts[leftChar]){
                    l++;
                    sInts[leftChar]--;
                }else if (tInts[leftChar] > 0 && sInts[leftChar] == tInts[leftChar]){
                    hashMap.put((i - l + 1),l);
                    count--;
                    sInts[leftChar]--;
                    l++;
                }

            }
        }
        if (hashMap.size() == 0){
            return "";
        }
        // 遍历map中最小的数
        Set<Integer> keySet = hashMap.keySet();
        int minLength = sLength + 1;
        for (Integer integer : keySet) {
            minLength = Math.min(integer, minLength);
        }
        Integer leftIndex = hashMap.get(minLength);
        return s.substring(leftIndex, leftIndex + minLength);
    }
}

我的解决方案是:

	int[] tInts = new int[128];
        int[] sInts = new int[128];

先把所有的t都加入到tInts中,在加sInts的过程中,如果发现这个加入的数据是必要的,就需要count++;通过这个count来维持这个是否满足要求;

总结: 1. 当碰到子串问题、子数组、字符串相关的问题时,多考虑滑动窗口。 2. 当碰到反转问题时,多想双指针。

数组相关问题之范围查询

image.png

这个题,k的相关问题,可以使用滑动窗口来进行解决;但是,对于t相关的问题,处理就比较有技巧了;这里涉及范围问题了;这个时候,就可以使用TreeMap的floor()函数来进行处理;

假设,TreeMap中有[a, b, c]这三个数,然后,需要加入d ,a, b, c中有一个数在[d - t, d + t]范围内,就可以放回ture; 这时候,我们要先知道TreeSet.floor(x)的作用,它的作用是:返回一个小于等于x的最大数; 很显然,小于d + t的数是都有可能满足条件的;这个时候,我们先找到TreeSet.floor(d + t) 来找到 小于d + t的最大数y 那么,这只是满足了第一个条件,它还需要大于等于TreeSet.floor(d - t);如果,同时满足这两个条件的话,就符合条件可以返回true了。

// 1. 加入一个数 d 需要判断之前的TreeMap中是否有数符合 [d - t, d + t]
// 2. 使用y = TreeSet.floor(d + t) 找到小于等于d + t的最大值,默认满足了 [d - t, d + t]中,d + t这个条件
// 3. y 还需要大于等于d - t;这样,才完全满足所有条件
    public  boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
        int length = nums.length;
        if (length < 2) {
            return false;
        }

        TreeSet<Double> treeMap = new TreeSet<>();
        treeMap.add((double)nums[0]);
        // 定义循环不变量[l , r]
        int l = 0;
        int r;
        for (r = 1; r < length; r++) {
            if (r - l <= k) {
                Double judgment = treeMap.floor((double)nums[r] + t);
                if (judgment != null && judgment >= (double)nums[r] - t) {
                    return true;
                }
            } else {
                treeMap.remove((double)nums[l]);
                l++;

                Double judgment = treeMap.floor((double)nums[r] + t);

                if (judgment != null && judgment >= (double)nums[r] - t) {
                    return true;
                }
            }
            treeMap.add((double)nums[r]);
        }
        return false;
    }