LeetCode刷题小结---数组篇

目录

  • 前言
  • 一、数组
      • 1.Java复习笔记
    • Arrays.sort()方法自定义排序:
    • Queue与Deque
    • PriorityQueue
      • 2.Java题解
        • 0.单调队列(必会模板题!)
        • 1. 最大子数组和:
        • 2. 加一:
        • 3. 寻找两个正序数组的中位数
        • 4. 盛最多水的容器
        • 5. 所有奇数长度子数组的和:
        • 6.顺时针打印矩阵
        • 7、异或!(数组中数字出现的次数)
        • 8.位运算加法
        • 9.搜索旋转排序数组
  • 总结

前言

本文用于记录刷LeetCode题过程中的心得与总结,同时也进行语言的复习(Java为主)。文章以题型分段。

一、数组

1.Java复习笔记

  • Java的数组长度在定义时长度就确定了,是定长数组。
int[] arr = new int[10];

那么要对数组添加元素时,可使用ArrayList。可以改变大小的数组为ArrayList,即可以定义一个ArrayList数组,然后用add(element)方法往里添加元素即可,还可add(index,element)往指定下标处添加元素。

ArrayList<int> list = new ArrayList<int>();

如果要将一个ArrayList类型的数组转化成int数组,似乎只能以循环赋值的方式转化。

  • int数组长度为arr.length,而ArrayList的长度为arrList.size()
  • int访问元素使用arr[i],而ArrayList访问使用arrList.get(i)
  • 数组的复制:如果使用arr1=arr2的方式将arr2的数据赋值给arr1是不行的,int数组的复制似乎只能以循环赋值的方式实现。
  • int与Integer的互相转换:
int a=3;
Integer A=Integer.valueOf(a);

Integer A=new Integer(5);
int a=A.intValue();
  • 整数数组排序:Arrays.sort(arr);

Arrays.sort()方法自定义排序:

LeetCode刷题小结---数组篇_第1张图片

  • ArrayList排序:
arr.sort(Comparator.naturalOrder());//升序
arr.sort(Comparator.reverseOrder());//降序

也可以

Collections.sort(arr);
  • 构造ArrayList数组的int数组:ArrayList al=new ArrayList<>();

  • 要判断一个数是否重复出现,使用哈希表是个很不错的选择:
    HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合(重复的元素不会被添加)。注意:HashSet会自动对其中元素进行排序

HashSet<String> sites = new HashSet<String>();
sites.add("abc");
......
return sites.contains("Taobao");
HashMap<Integer, String> Sites = new HashMap<Integer, String>();
Sites.put(4, "Zhihu");
Sites.get(3)
Sites.remove(4);
if(sites.containsKey(1)) {
    System.out.println("key 为 1 存在于 sites 中");
}

hashmap.replace(K key, V newValue);

遍历元素
for (Integer i : Sites.keySet()) {
            System.out.println("key: " + i + " value: " + Sites.get(i));
}
或者:
h.forEach((key, value) -> {
            value = value - value * 10/100;
            System.out.print(key + "=" + value + " ");
});

对于需要对HashMap的key进行排序的情况,可使用TreeMap代替HashMap.
TreeMap默认按照自然顺序对键排序,对数字按升序,字符串按照字母顺序。
其内部基于红黑树实现

Queue与Deque

Queue方法:offer()添加队列元素, poll()弹出队头元素,peek()查看队头元素

Deque:实例化:Deque deque = new LinkedList<>();
方法:
offerLast() offerFirst()
pollLast() pollFirst()
peekLast() peekFirst()

PriorityQueue

逻辑上使用堆结构(完全二叉树)实现,物理上使用动态数组实现,并非像TreeMap一样完全有序,但是如果按照指定方式出队,结果可以是有序的。
PriorityQueue总是先输出根节点的值,然后调整树使之继续成为一棵完全二叉树 样每次输出的根节点总是整棵树优先级最高的,要么数值最小要么数值最大
而我们每次的调整结构都是不断按照某种规则比较两个元素的值大小,然后调整结构,这里就需要用到我们的比较器。所以构建一个PriorityQueue实例主要还是初始化数组容量和comparator比较器

重写比较函数类似自定义sort排序

//小根堆
PriorityQueue<ListNode> pq = new PriorityQueue<>(new Comparator<ListNode>() {
            @Override
            public int compare(ListNode o1, ListNode o2) {
                return o1.val - o2.val;
            }
});

public boolean add(E e); //在队尾插入元素,插入失败时抛出异常,并调整堆结构
public boolean offer(E e); //在队尾插入元素,插入失败时抛出false,并调整堆结构

public E remove(); //获取队头元素并删除,并返回,失败时前者抛出异常,再调整堆结构
public E poll(); //获取队头元素并删除,并返回,失败时前者抛出null,再调整堆结构

public E element(); //返回队头元素(不删除),失败时前者抛出异常
public E peek();//返回队头元素(不删除),失败时前者抛出null

public boolean isEmpty(); //判断队列是否为空
public int size(); //获取队列中元素个数
public void clear(); //清空队列
public boolean contains(Object o); //判断队列中是否包含指定元素(从队头到队尾遍历)
public Iterator iterator(); //迭代器

2.Java题解

0.单调队列(必会模板题!)

LeetCode刷题小结---数组篇_第2张图片


public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length == 0 || k == 0) return new int[0];

        //单调队列
        Deque<Integer> deque = new LinkedList<>();
        int[] res = new int[nums.length - k + 1];
        for(int j = 0, i = 1 - k; j < nums.length; i++, j++) {
            // 删除 deque 中对应的 如果队中最大数是左边移出的数,则从队中移除此数
            if(i > 0 && deque.peekFirst() == nums[i - 1])
                deque.removeFirst();
            // 保持 deque 递减,从后往前看,只要新加入的数大于队中的数,则从队列中移除队中的数
            while(!deque.isEmpty() && deque.peekLast() < nums[j])
                deque.removeLast();
            //直到找到比新数大的数,或者队空,就停止,然后把新数加进去
            deque.addLast(nums[j]);
            // 记录窗口最大值
            if(i >= 0)
                res[i] = deque.peekFirst();
        }
        return res;
    }

1. 最大子数组和:

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
动态规划:

// Kadane算法扫描一次整个数列的所有数值,
// 在每一个扫描点计算以该点数值为结束点的子数列的最大和(正数和)。
// 该子数列由两部分组成:以前一个位置为结束点的最大子数列、该位置的数值。
// 因为该算法用到了“最佳子结构”(以每个位置为终点的最大子数列都是基于其前一位置的最大子数列计算得出, 
// 该算法可看成动态规划的一个例子。
// 状态转移方程:sum[i] = max{sum[i-1]+a[i],a[i]}   
// 其中(sum[i]记录以a[i]为子序列末端的最大序子列连续和)

public int maxSubArray(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];
        dp[0] = nums[0];

        int max = nums[0];
        for (int i = 1; i < n; i++) {
            dp[i] = Math.max(dp[i- 1] + nums[i], nums[i]);
            max = Math.max(max, dp[i]);
        }
        return max;
}
2. 加一:

给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。
你可以假设除了整数 0 之外,这个整数不会以零开头。
输入:digits = [1,2,3]
输出:[1,2,4]

public int[] plusOne(int[] digits) {
        int len=digits.length;
        int[] digits2=new int[len+1];
        for(int i=len-1;i>=0;i--){
            if(digits[i]!=9){
                digits[i]++;
                return digits;
            }else{
                digits[i]=0;
            }
        }
        digits2[0]=1;
        return digits2;
}

这道题的重点就是对于进位的处理。这类题如果选择从后往前的遍历,处理起来会简便很多,思路是:从后往前依次扫描数字,若非9,则加1并直接返回;若是9则置为0,继续向前扫描下一个数字。

3. 寻找两个正序数组的中位数

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n))

这道题让我们求两个有序数组的中位数,而且限制了时间复杂度为O(log (m+n)),看到这个时间复杂度,自然而然的想到了应该使用二分查找法来求解。那么回顾一下中位数的定义,如果某个有序数组长度是奇数,那么其中位数就是最中间那个,如果是偶数,那么就是最中间两个数字的平均值。这里对于两个有序数组也是一样的,假设两个有序数组的长度分别为m和n,由于两个数组长度之和 m+n 的奇偶不确定,因此需要分情况来讨论,对于奇数的情况,直接找到最中间的数即可,偶数的话需要求最中间两个数的平均值。为了简化代码,不分情况讨论,我们使用一个小trick,我们分别找第 (m+n+1) / 2 个,和 (m+n+2) / 2 个,然后求其平均值即可,这对奇偶数均适用。加入 m+n 为奇数的话,那么其实 (m+n+1) / 2 和 (m+n+2) / 2 的值相等,相当于两个相同的数字相加再除以2,还是其本身。

这里我们需要定义一个函数来在两个有序数组中找到第K个元素,下面重点来看如何实现找到第K个元素。首先,为了避免产生新的数组从而增加时间复杂度,我们使用两个变量i和j分别来标记数组nums1和nums2的起始位置。然后来处理一些边界问题,比如当某一个数组的起始位置大于等于其数组长度时,说明其所有数字均已经被淘汰了,相当于一个空数组了,那么实际上就变成了在另一个数组中找数字,直接就可以找出来了。还有就是如果K=1的话,那么我们只要比较nums1和nums2的起始位置i和j上的数字就可以了。难点就在于一般的情况怎么处理?因为我们需要在两个有序数组中找到第K个元素,为了加快搜索的速度,我们要使用二分法,对K二分,意思是我们需要分别在nums1和nums2中查找第K/2个元素,注意这里由于两个数组的长度不定,所以有可能某个数组没有第K/2个数字,所以我们需要先检查一下,数组中到底存不存在第K/2个数字,如果存在就取出来,否则就赋值上一个整型最大值。如果某个数组没有第K/2个数字,那么我们就淘汰另一个数字的前K/2个数字即可。有没有可能两个数组都不存在第K/2个数字呢,这道题里是不可能的,因为我们的K不是任意给的,而是给的m+n的中间值,所以必定至少会有一个数组是存在第K/2个数字的。最后就是二分法的核心啦,比较这两个数组的第K/2小的数字midVal1和midVal2的大小,如果第一个数组的第K/2个数字小的话,那么说明我们要找的数字肯定不在nums1中的前K/2个数字,所以我们可以将其淘汰,将nums1的起始位置向后移动K/2个,并且此时的K也自减去K/2,调用递归。反之,我们淘汰nums2中的前K/2个数字,并将nums2的起始位置向后移动K/2个,并且此时的K也自减去K/2,调用递归即可。

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int m = nums1.length;
        int n = nums2.length;
        int left = (m + n + 1) / 2;
        int right = (m + n + 2) / 2;
        return (findKth(nums1, 0, nums2, 0, left) + findKth(nums1, 0, nums2, 0, right)) / 2.0;
    }
    //i: nums1的起始位置 j: nums2的起始位置
    public int findKth(int[] nums1, int i, int[] nums2, int j, int k){
        if( i >= nums1.length) return nums2[j + k - 1];//nums1为空数组
        if( j >= nums2.length) return nums1[i + k - 1];//nums2为空数组
        if(k == 1){
            return Math.min(nums1[i], nums2[j]);
        }
        int midVal1 = (i + k / 2 - 1 < nums1.length) ? nums1[i + k / 2 - 1] : Integer.MAX_VALUE;
        int midVal2 = (j + k / 2 - 1 < nums2.length) ? nums2[j + k / 2 - 1] : Integer.MAX_VALUE;
        if(midVal1 < midVal2){
            return findKth(nums1, i + k / 2, nums2, j , k - k / 2);
        }else{
            return findKth(nums1, i, nums2, j + k / 2 , k - k / 2);
        }        
    }
4. 盛最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。

对O(n)的算法写一下自己的理解,一开始两个指针一个指向开头一个指向结尾,此时容器的底是最大的,接下来随着指针向内移动,会造成容器的底变小,在这种情况下想要让容器盛水变多,就只有在容器的高上下功夫。 那我们该如何决策哪个指针移动呢?我们能够发现不管是左指针向右移动一位,还是右指针向左移动一位,容器的底都是一样的,都比原来减少了 1。这种情况下我们想要让指针移动后的容器面积增大,就要使移动后的容器的高尽量大,所以我们选择指针所指的高较小的那个指针进行移动,这样我们就保留了容器较高的那条边,放弃了较小的那条边,以获得有更高的边的机会。

public int maxArea(int[] height) {
        int max=0,l=0,r=height.length-1;
        max=0;
        while(l<r){
            max=Math.max(max,(r-l)*Math.min(height[l],height[r]));
            if(height[l]>=height[r]){
                r--;
            }else{
                l++;
            }
        }
    return max;
}
5. 所有奇数长度子数组的和:

给你一个正整数数组 arr ,请你计算所有可能的奇数长度子数组的和。
子数组 定义为原数组中的一个连续子序列。
请你返回 arr 中 所有奇数长度子数组的和 。

LeetCode刷题小结---数组篇_第3张图片

6.顺时针打印矩阵

LeetCode刷题小结---数组篇_第4张图片
注意:矩阵不一定长宽相等!


class Solution {
    public int[] spiralOrder(int[][] matrix) {
        if(matrix.length == 0) return new int[0];
        int l = 0, r = matrix[0].length - 1, t = 0, b = matrix.length - 1, x = 0;
        int[] res = new int[(r + 1) * (b + 1)];

        while(true) {
            for(int i = l; i <= r; i++) {
                res[x++] = matrix[t][i]; // left to right.
            }
            t++;
            if(t > b) {
                break;
            }
            for(int i = t; i <= b; i++) {
                res[x++] = matrix[i][r]; // top to bottom.
            }
            r--;
            if(l > r){
                break;
            }
                
            for(int i = r; i >= l; i--){
                res[x++] = matrix[b][i]; // right to left.
            } 
            b--;
            if(t > b){
                break;
            } 
            for(int i = b; i >= t; i--) {
                res[x++] = matrix[i][l]; // bottom to top.
            }
            l++;
            if(l > r) {
                break;
            }
        }
        return res;
    }
}

7、异或!(数组中数字出现的次数)

LeetCode刷题小结---数组篇_第5张图片


让我们先来考虑一个比较简单的问题:

如果除了一个数字以外,其他数字都出现了两次,那么如何找到出现一次的数字?

答案很简单:全员进行异或操作即可。考虑异或操作的性质:对于两个操作数的每一位,相同结果为 0,不同结果为 1。那么在计算过程中,成对出现的数字的所有位会两两抵消为 0,最终得到的结果就是那个出现了一次的数字。
LeetCode刷题小结---数组篇_第6张图片
举例来说:[1,2,2,3,3,4,4,5]=[001,010,010,011,011,100,100,101]
所有异或的结果为 001^101=100,代表这里面的不同的两个数的二进制的第三位是不同的。按第三位给原来的序列分组,则必定能把x和y分到不同组。分为[001,010,010,011,011]和[100,100,101],对这两个序列依次异或即可得到答案。

当然,如何根据二进制的某一位区分也需要二进制运算的技巧。
可以利用一个div=1依次向左移位之后再与x^y进行与运算,当结果不为0时,代表发现从低位到高位的第一个1,div便可记录此位置。然后再用这个div对nums中元素进行与运算,将结果为0与结果不为0 的分成两组,每组再依次进行异或运算,最后两组各自的结果就是最终结果。

public int[] singleNumbers(int[] nums) {
        int ret = 0;
        for (int n : nums) {
            ret ^= n;
        }
        int div = 1;
        while ((div & ret) == 0) {
            div <<= 1;
        }
        int a = 0, b = 0;
        for (int n : nums) {
            if ((div & n) != 0) {
                a ^= n;
            } else {
                b ^= n;
            }
        }
        return new int[]{a, b};
    }

既然涉及位运算了,不如再来个经典位运算题

8.位运算加法

LeetCode刷题小结---数组篇_第7张图片


^ 异或运算 相当于 无进位的求和, 想象10进制下的模拟情况:(如:19+1=20;无进位求和就是10,而非20;因为它不管进位情况)

& 与运算 相当于求每位的进位数, 先看定义:1&1=1;1&0=0;0&0=0;即都为1的时候才为1,正好可以模拟进位数的情况,还是想象10进制下模拟情况:(9+1=10,如果是用&的思路来处理,则9+1得到的进位数为1,而不是10,所以要用<<1向左再移动一位,这样就变为10了);

这样公式就是:(a^b) ^ ((a&b)<<1) 即:每次无进位求 + 每次得到的进位数--------我们需要不断重复这个过程,直到进位数为0为止;

class Solution {
    public int add(int a, int b) {
        while(b != 0) { // 当进位为 0 时跳出
            int c = (a & b) << 1;  // c = 进位
            a ^= b; // a = 非进位和
            b = c; // b = 进位
        }
        return a;
    }
}

由于其中一些变量的转换比较繁琐,可以考虑直接背下来,防止懂原理但是搓不出来的情况。

9.搜索旋转排序数组

总结

你可能感兴趣的:(秋招日寄,leetcode,动态规划,java)