目录
合并两个有序数组
题目描述
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。
示例 3:
输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。
提示:
nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-109 <= nums1[i], nums2[j] <= 109
进阶:你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?
解题
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int len1 = m - 1;
int len2 = n - 1;
int len = m + n - 1;
while(len1 >= 0 && len2 >= 0) {
nums1[len--] = nums1[len1] > nums2[len2] ? nums1[len1--] : nums2[len2--];
}
// 表示将nums2数组从下标0位置开始,拷贝到nums1数组中,从下标0位置开始,长度为len2+1
System.arraycopy(nums2, 0, nums1, 0, len2 + 1);
}
}
复杂性分析
- 时间复杂度: O(m+n)
- 空间复杂度: O(1)
找到字符串中所有字母异位词
1 题目描述
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指字母相同,但排列不同的字符串。
示例 1:
输入: s = “cbaebabacd”, p = “abc”
输出: [0,6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的异位词。
起始索引等于 6 的子串是 “bac”, 它是 "abc"的异位词。
示例 2:
输入: s = “abab”, p = “ab”
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 “ab”, 它是 "ab"的异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的异位词。 起始索引等于 2 的子串是 “ab”, 它是 "ab"的异位词。
提示:
- 1 <= s.length, p.length <= 3 * 104
- s 和 p 仅包含小写字母
2 解题(Java)
滑动窗口+字符运算:
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> res = new ArrayList<>();
if (s.length() < p.length()) {
return res;
}
int[] sArray = new int[26];
int[] pArray = new int[26];
for (int i=0; i<p.length(); i++) {
sArray[s.charAt(i) - 'a']++;
pArray[p.charAt(i) - 'a']++;
}
if (Arrays.equals(sArray,pArray)) {
res.add(0);
}
for (int i=p.length(); i<s.length(); i++) {
sArray[s.charAt(i-p.length()) - 'a']--;
sArray[s.charAt(i) - 'a']++;
if (Arrays.equals(sArray,pArray)) {
res.add(i - p.length() + 1);
}
}
return res;
}
}
3 复杂性分析
- 时间复杂度:O(n),for循环O(n),数组长度为常数,因此数组的比较也是常数级,总时间复杂度为O(n);
- 空间复杂度:O(1);
移动零
1 题目描述
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
- 必须在原数组上操作,不能拷贝额外的数组。
- 尽量减少操作次数。
2 解题(Java)
快排思想:
class Solution {
public void moveZeroes(int[] nums) {
if(nums == null) return;
int left = 0;
for(int right = 0; right < nums.length; right++) {
if(nums[right] != 0) {
int tmp = nums[right];
nums[right] = nums[left];
nums[left++] = tmp;
}
}
}
}
3 复杂性分析
- 时间复杂度: O(n)
- 空间复杂度: O(1)
滑动窗口的最大值
1 题目描述
给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
示例:
提示:
你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。
2 解题(Java)
2.1 解题思路
本题难点: 如何在每次窗口滑动后,将 “获取窗口内最大值” 的时间复杂度从 O(k) 降低至 O(1);
回忆 包含min函数的栈,其使用 单调栈 实现了随意入栈、出栈情况下的 O(1) 时间获取 “栈内最小值” ,本题同理。
窗口对应的数据结构为 双端队列 ,本题使用 单调队列 即可解决以上问题。遍历数组时,每轮保证单调非严格递减队列 deque:
- deque 内仅包含窗口内的元素 ⇒ 每轮窗口滑动移除了元素 nums[left−1] ,如果上一个窗口[left-1,right-1]最大值deque.peekFirst()等于nums[left-1],还需删除deque的队头元素;
- deque 内的元素 非严格递减 ⇒ 每轮窗口滑动添加了元素 nums[right + 1],需将 deque 内所有 < nums[right + 1] 的元素删除;
2.2 算法流程
- 初始化: 双端队列deque ,结果列表res,数组长度 n;
- 滑动窗口: 左边界范围 left∈[1−k,n−k] ,右边界范围 right∈[0,n−1] ;
- 若 left > 0 且 队首元素 deque[0] == 被删除元素 nums[left - 1]:则队首元素出队;
- 删除 deque 内所有<nums[right] 的元素,以保持 deque 非严格递减;
- 将 nums[right] 添加至 deque 尾部;
- 若已形成窗口(left≥0 ):将窗口最大值(即队首元素 deque[0])添加至数组 res;
- 返回值: 返回结果列表 res ;
2.3 代码
class Solution {
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 right=0,left=1-k; right < nums.length; left++, right++) {
// 保持deque非严格递减
while (!deque.isEmpty() && deque.peekLast() < nums[right]) {
deque.pollLast();
}
deque.offer(nums[right]);
if (left >= 0) {
res[left] = deque.peek();// 记录窗口最大值
}
// 如果窗口[left,right]最大值恰好就是nums[left],右移之后最大值作废,所以删除队列头
if (left >= 0 && deque.peek() == nums[left]) {
deque.poll();
}
}
return res;
}
}
3 复杂性分析
- 时间复杂度 O(N) : 其中 N 为数组 nums 长度;线性遍历 nums 占用 O(N);每个元素最多仅入队和出队一次,因此单调队列 deque 占用 O(2N) ;
- 空间复杂度 O(k) : 双端队列 deque 中最多同时存储 k 个元素(即窗口大小);
*和为s的连续正数序列
1 题目描述
输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
示例 1:
输入:target = 9
输出:[[2,3,4],[4,5]]
示例 2:
输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]
限制:
1 <= target <= 10^5
2 解题(Java)
2.1 解题思路
- 设连续正整数序列的左边界 i 和右边界 j ,构建滑动窗口从左到右滑动;
- 循环中,每轮判断滑动窗口内元素和s与目标值target的大小关系;
- 如果相等,记录结果并向右移动左边界i(尝试其他方案);如果大于target,向右移动左边界i(减小窗口内的元素和);如果小于target,向右移动右边界(增大窗口内的元素和);
2.2 算法流程
-
初始化:左边界left = 1,右边界right = 2,元素和sum = 3,结果列表res;
-
循环:当left >= right时跳出:
- 如果sum > target:向右移动左边界left = left + 1,并更新元素和sum;
- 如果sum < target:向右移动右边界right = right + 1,并更新元素和sum;
- 如果sum = target;记录连续整数序列,并向右移动左边界 left = left + 1;
-
返回值:返回结果列表res;
2.3 代码
class Solution {
public int[][] findContinuousSequence(int target) {
int left = 1, right = 2, sum = 3;
List<int[]> res = new LinkedList<>();
while (left < right) {
if (sum == target) {
int[] ans = new int[right - left + 1];
for (int i = left; i <= right; i++) {
ans[i - left] = i;
}
res.add(ans);
}
if (sum >= target) {
sum -= left;
left++;
} else {
right++;
sum += right;
}
}
return res.toArray(new int[][]{});
}
}
3 复杂性分析
- 时间复杂度 O(N): 其中 N = target,连续整数序列至少有两个数字,而 left < right 恒成立,因此至多循环 target( left, right 都移动到target/2),使用 O(N) 时间;
- 空间复杂度O(1):除了答案数组外,变量 left , right , sum 使用常数大小的额外空间;
*最小覆盖子串
1 题目描述
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
示例 2:
输入:s = “a”, t = “a”
输出:“a”
提示:
- 1 <= s.length, t.length <= 10 ^ 5
- s 和 t 由英文字母组成
进阶:你能设计一个在 o(n) 时间内解决此问题的算法吗?
2 解题(Java)
class Solution {
public String minWindow(String s, String t) {
Map<Character, Integer> tMap = new HashMap<>();
Map<Character, Integer> windowMap = new HashMap<>();
// 记录t中所有字符及其出现的次数
for (char c : t.toCharArray()) {
tMap.put(c, tMap.getOrDefault(c, 0) + 1);
}
int left = 0, right = 0;
// 记录窗口中满足条件的字符个数
int count = 0;
// 记录最小覆盖子串的起始索引及长度
int start = 0, minLength = Integer.MAX_VALUE;
while (right < s.length()) {
char c = s.charAt(right);
// 判断取出的字符是否在t中
if (tMap.containsKey(c)) {
windowMap.put(c, windowMap.getOrDefault(c, 0) + 1);
// 判断取出的字符在窗口中出现的次数是否与t中该字符的出现次数相同
if (windowMap.get(c).equals(tMap.get(c))) {
count++;
}
}
// 找到符合条件的子串后,尝试缩小窗口
while (count == tMap.size()) {
if (right - left + 1 < minLength) {
start = left;
minLength = right - left + 1;
}
char c1 = s.charAt(left);
left++;
if (tMap.containsKey(c1)) {
if (windowMap.get(c1).equals(tMap.get(c1))) {
count--;
}
windowMap.put(c1, windowMap.get(c1) - 1);
}
}
// 尝试新方案
right++;
}
return minLength == Integer.MAX_VALUE ? "" : s.substring(start, start + minLength);
}
}
3 复杂性分析
- 时间复杂度O(n):n为s的长度,线性遍历一次s;
- 空间复杂度O(n):HashMap所占空间;
*调整数组顺序使奇数位于偶数前面
1 题目描述
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
示例:
输入:nums = [1,2,3,4]
输出:[1,3,2,4]
注:[3,1,2,4] 也是正确的答案之一。
2 解题(Java)
class Solution {
public int[] exchange(int[] nums) {
int left = 0, right = nums.length - 1;
while (left < right) {
while (left < right && (nums[left] & 1) == 1) {
left++;
}
while (left < right && (nums[right] & 1) == 0) {
right--;
}
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
return nums;
}
}
3 复杂性分析
- 时间复杂度 O(N): N 为数组 nums 长度,双指针 left, right 共同遍历整个数组;
- 空间复杂度 O(1) : 双指针 left, right 使用常数大小的额外空间;
*和为s的两个数字
1 题目描述
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
示例 2:
输入:nums = [10,26,30,31,47,60], target = 40
输出:[10,30] 或者 [30,10]
限制:
1 <= nums.length <= 10 ^ 5
1 <= nums[i] <= 10^6
2 解题(Java)
2.1 解题思路
- 利用哈希表可以通过遍历数组找到数字组合,时间和空间复杂度均为O(N);
- 由于本题的nums是排序数组,因此可以使用双指针法将空间复杂度降至O(1);
算法流程:
-
初始化:双指针left,right分别指向数组nums的左右两端(对撞双指针);
-
循环搜索(当双指针相遇时跳出):
- 计算s = nums[left] + nums[right];
- 若s > target,right左移,即right = right - 1;
- 若s < target,left右移,即left = left + 1;
- 若s = target,立刻返回数组[nums[left], nums[right]];
-
返回空数组,表示不存在和为target的组合;
2.2 代码
class Solution {
public int[] twoSum(int[] nums, int target) {
// 定义左指针和右指针
int left = 0, right = nums.length - 1;
// 当双指针相遇时跳出
while(left < right) {
int s = nums[left] + nums[right];
if(s < target) left++;
else if(s > target) right--;
else return new int[] { nums[left], nums[right] };
}
return new int[0];
}
}
3 复杂性分析
- 时间复杂度 O(N) : N 为数组 nums 的长度,双指针共同线性遍历整个数组;
- 空间复杂度 O(1) : 变量 left, right 占用常数大小的额外空间;
*链表中倒数第k个节点
1 题目描述
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
2 解题(Java)
2.1 解题思路
使用双指针:
- 初始化: 前指针 former 、后指针 latter ,双指针都指向头节点 head;
- 构建双指针距离: 前指针former先向前走k步(结束后,双指针 former 和 latter 间相距 k 步);
- 双指针共同移动: 循环中,双指针former和latter每轮都向前走一步,直至former走过链表尾节点时跳出(跳出后,latter与尾节点距离为 k−1,即latter指向倒数第k个节点);
- 返回值: 返回latter即可;
- 考虑越界问题:former在前k次移动时,每次移动前,检查是否为null,如果是,则越界,返回null;
2.2 代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode former = head, latter = head;
// former先走k步
for(int i = 0; i < k; i++) {
// 考虑越界情况
if(former == null) return null;
former = former.next;
}
while(former != null) {
former = former.next;
latter = latter.next;
}
return latter;
}
}
3 复杂性分析
- 时间复杂度 O(N) : N 为链表长度,former 走了 N 步, latter 走了 (N−k) 步;
- 空间复杂度 O(1): 双指针 former , latter 使用常数大小的额外空间;
*反转链表
1 题目描述
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
限制:
0 <= 节点个数 <= 5000
2 解题(Java)
2.1 解题思路
共定义3个指针:
temp暂存后继结点,cur修改引用指向,pre暂存当前结点,cur访问后继结点。
2.2 算法流程
2.3 代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
ListNode cur = head, pre = null;
while(cur != null) {
ListNode tmp = cur.next; // 暂存后继结点
cur.next = pre; // 修改引用指向
pre = cur; // 暂存当前结点
cur = tmp; // 访问下一结点
}
return pre;
}
}
3 复杂性分析
- 时间复杂度 O(N): 遍历链表使用线性大小时间。
- 空间复杂度 O(1): 变量 pre 和 cur 使用常数大小额外空间。
*翻转单词顺序
1 题目描述
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. “,则输出"student. a am I”。
示例 1:
输入: “the sky is blue”
输出: “blue is sky the”
示例 2:
输入: " hello world! "
输出: “world! hello”
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
示例 3:
输入: “a good example”
输出: “example good a”
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
说明:
- 无空格字符构成一个单词。
- 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
- 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
2 解题(Java)
2.1 解题思路
- 定义双指针left和right作为单词的左右索引边界,初始指向字符串s的末尾;
- 倒序遍历s,每确定一个单词边界,就将其添加至res;
- 最后,将res转成字符串,并去掉末尾的空格;
2.2 代码
class Solution {
public String reverseWords(String s) {
s = s.trim(); // 删除首尾空格
int right = s.length() - 1, left = right;
StringBuilder res = new StringBuilder();
while(left >= 0) {
while(left >= 0 && s.charAt(left) != ' ') left--; // 搜索首个空格
res.append(s.substring(left + 1, right + 1) + " "); // 添加单词
while(left >= 0 && s.charAt(left) == ' ') left--; // 跳过单词间空格
right = left; // j 指向下个单词的尾字符
}
return res.toString().trim(); // 转化为字符串并返回
}
}
(也可以利用 “字符串分割” 的内置函数split,再数组倒序的方法完成,面试不建议使用)
3 复杂性分析
- 时间复杂度 O(N) : 其中 N 为字符串 s 的长度,线性遍历字符串使用O(N)时间;
- 空间复杂度 O(N) : StringBuilder占用 O(N) 大小的额外空间;
*盛最多水的容器
1 题目描述
给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器。
示例 1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:
输入:height = [1,1]
输出:1
示例 3:
输入:height = [4,3,2,1,4]
输出:16
示例 4:
输入:height = [1,2,1]
输出:2
提示:
- n = height.length
- 2 <= n <= 3 * 10 ^ 4
- 0 <= height[i] <= 3 * 10 ^ 4
2 解题(Java)
2.1 算法流程
设置双指针 left,right分别位于容器壁两端,根据规则移动指针,并且更新面积最大值 res,直到 left == right 时返回 res。
2.2 指针移动规则与证明
每次选定围成水槽两板高度h[left]和h[right]中的短板,向中间收窄 1 格。以下证明:
- 设每一状态下水槽面积为 S(left, right)(0 <= left < right < n),由于水槽的实际高度由两板中的短板决定,则可得面积公式 S(left, right) = min(h[left], h[right]) × (j - i);
- 在每一个状态下,无论长板或短板收窄 1 格,都会导致水槽底边宽度 -1:
- 若向内移动短板,水槽的短板 min(h[left], h[right])可能变大,因此水槽面积 S(left, right)可能增大;
- 若向内移动长板,水槽的短板 min(h[left], h[right])不变或变小,下个水槽的面积一定小于当前水槽面积;
因此,向内收窄短板可以获取面积最大值。
2.3 代码
class Solution {
public int maxArea(int[] height) {
int left = 0, right = height.length - 1, res = 0;
while(left < right){
res = height[left] < height[right] ? Math.max(res, (right - left) * height[left++]) : Math.max(res, (right - left) * height[right--]);
}
return res;
}
}
3 复杂性分析
- 时间复杂度 O(N):双指针遍历一次数组;
- 空间复杂度 O(1):双指针占用常数额外空间;
*三数之和
1 题目描述
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
2 解题(Java)
2.1 解题思路
- 暴力法搜索为 O(N^3) 时间复杂度,可通过双指针动态消去无效解来优化效率;
- 双指针法铺垫: 先将给定 nums 排序,时间复杂度为 O(NlogN);
- 三指针法思路: 固定 3 个指针中最左(最小)数字的指针 index,双指针 left,right 分设在数组索引 (index+1,len(nums)-1) 两端,通过双指针交替向中间移动,记录对于每个固定指针 index 的所有满足 nums[index] + nums[left] + nums[right] == 0 的 left,right 组合:
- 当 nums[index] > 0 时直接break跳出:因为 nums[right] >= nums[left] >= nums[index] > 0,即 3 个数字都大于 0 ,在此固定指针 index 之后不可能再找到结果了;
- 当 index > 0且nums[index] == nums[index - 1]时即跳过此元素nums[index]:nums[k - 1] 的组合已经包含了nums[k]的所有组合,本次双指针搜索只会得到重复组合;
- left,right 分设在数组索引 (index+1, len(nums)-1) 两端,当left < right时循环计算s = nums[index] + nums[left] + nums[right],并按照以下规则执行双指针移动:
- 当s < 0时,left++并跳过所有重复的nums[left];
- 当s > 0时,right–并跳过所有重复的nums[right];
- 当s == 0时,记录组合[index, left, right]至res,执行left++ 和 right-- 并跳过所有重复的nums[left]和nums[right],防止记录到重复组合;
2.2 代码
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new LinkedList<>();
for (int index = 0; index < nums.length - 2; index++) {
if (nums[index] > 0) break;
if (index > 0 && nums[index] == nums[index - 1]) continue;
int left = index + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[index] + nums[left] + nums[right];
if (sum < 0) {
while (left < right && nums[left] == nums[++left]);
} else if (sum > 0) {
while (left < right && nums[right] == nums[--right]);
} else {
res.add(Arrays.asList(nums[index], nums[left], nums[right]));
while (left < right && nums[left] == nums[++left]);
while (left < right && nums[right] == nums[--right]);
}
}
}
return res;
}
}
3 复杂性分析
- 时间复杂度 O(N^2):其中外循环指针index遍历数组使用O(N)时间,内循环双指针left和right 遍历数组使用O(N)时间,共使用O(N ^ 2)时间;
- 空间复杂度 O(1):指针占用常数大小的额外空间;