1. 哈希
1.1 两数之和
在数组中找到两个元素的数值之和为目标值,返回这两个数组的下标。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
思路
:暴力解法是使用两层for循环查找元素,时间复杂度是O(n2)。
如果使用哈希法,本题可以使用map,分析一下使用数组和set的局限:
- 数组的长度是受限制的,如果元素很少而哈希值很大,则会造成内存空间的浪费。
- set是一个集合,里面存放的元素只能是一个key,而本地不仅要判断y是否存在,而且还要记录y的下标位置,因为要返回x和y的小标,所以set也不能用。
此时选择另外一种数据结构——map,<key,value>的存储结构,可以使用key保存数值,用value保存数值所在的下标。
力扣代码:
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> map = new HashMap<>();
for(int i = 0; i < nums.length; i++){
int curNum = nums[i];
if(map.containsKey(target - curNum)){
return new int[]{map.get(target - curNum), i};
}
//key保存数值,用value保存数之所在的下标
map.put(curNum, i);
}
return new int[0];
}
}
1.2 字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
输入: strs = [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]
输出: [[“bat”],[“nat”,“tan”],[“ate”,“eat”,“tea”]]
示例 2:
输入: strs = [“”]
输出: [[“”]]
示例 3:
输入: strs = [“a”]
输出: [[“a”]]
思路
:两个字符串互为字母异位词,当且仅当两个字符串包含的字母相同。同一组字母异位词中的字符串具备相同点,可以使用相同点作为一组字母异位词的标志,使用哈希表存储每一组字母异位词,哈希表的键为一组字母异位词的标志,哈希表的值为一组字母异位词列表
- 初始化一个map,用于存放相同的一组字母异位词
- 遍历字符串数组,并对字符串的字符进行排序
- 排序后的字符串作为key,检测map中是否存在该key:进行分情况处理
力扣代码:
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
//初始化一个map
Map<String,List<String>> map = new HashMap<>();
for(String str : strs){
//转为char数组排序
char[] array = str.toCharArray();
Arrays.sort(array);
//拿到排序后的字符串
String key = new String(array);
if(map.containsKey(key)){
//如果已存在key,取出值list,将该字符串str加入list,重新放入map
List<String> list = map.get(key);
list.add(str);
map.put(key,list);
}else{
//如果不存在key,创建一个新的list,将该字符串str加入list,放入map
List<String> list = new ArrayList<>();
list.add(str);
map.put(key,list);
}
}
//返回map.values()
return new ArrayList<>(map.values());
}
}
1.3 最长连续序列
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
示例 3:
输入:nums = [1,0,1,2]
输出:3
思路
:考虑枚举数组中的每个数 x,考虑以其为起点,不断尝试匹配 x+1,x+2,⋯ 是否存在,假设最长匹配到了 x+y,那么以 x 为起点的最长连续序列即为 x,x+1,x+2,⋯,x+y,其长度为 y+1,我们不断枚举并更新答案即可。但这样达不到O(n),所以我们在枚举时,可以先进性判断x-1是否存在,来进行是否以x作为开头。
- 创建一个set用于存在去重后的数组元素
- 遍历set
- 如果不包含(num-1),就以该num作为头进行计算最长连续。
- 如果包含数值num的下一个数字,进行++操作
力扣代码:
class Solution {
public int longestConsecutive(int[] nums) {
//创建一个set用于存在数组中的元素(去重)
Set<Integer>set = new HashSet<>();
for(int num : nums){
set.add(num);
}
int longest = 0;
//遍历set
for(int num : set){
//如果不包含(num-1),就以该num作为头进行计算最长连续
if(!set.contains(num - 1)){
int currentNum = num;
int currentLongest = 1;
//如果包含数值num的下一个数字,进行++操作
while(set.contains(currentNum + 1)){
currentNum += 1;
currentLongest += 1;
}
//比较当前num的最大连续数与longest的较大值,赋值给longest
longest = Math.max(longest,currentLongest);
}
}
return longest;
}
}
2. 双指针
2.1 移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
思路
:
使用双指针,左指针指向当前已经处理好的序列的尾部,右指针指向待处理序列的头部。
右指针不断向右移动,每次右指针指向非零数,则将左右指针对应的数交换,同时左指针右移。
注意到以下性质:
- 左指针左边均为非零数;
- 右指针左边直到左指针处均为零。
因此每次交换,都是将左指针的零与右指针的非零数交换,且非零数的相对顺序并未改变。
力扣代码:
class Solution {
public void moveZeroes(int[] nums) {
int l = 0, r = 0, n = nums.length;
while(r < n){
//如果右指针指向的是非零数,进行左右交换
if(nums[r] != 0){
swap(nums,l,r);
l++;
}
r++;
}
}
//左右交换
public void swap(int[] nums, int left, int right){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
2.1 盛水最多的容器
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 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
思路
:
在初始时,左右指针分别指向数组的左右两端,它们可以容纳的水量为 min(1,7)∗8=8。
此时我们需要移动一个指针。移动哪一个呢?直觉告诉我们,应该移动对应数字较小的那个指针(即此时的左指针)。这是因为,由于容纳的水量是由
两个指针指向的数字中较小值∗指针之间的距离
决定的。如果我们移动数字较大的那个指针,那么前者「两个指针指向的数字中较小值」不会增加,后者「指针之间的距离」会减小,那么这个乘积会减小。因此,我们移动数字较大的那个指针是不合理的。因此,我们移动 数字较小的那个指针。
力扣代码
class Solution {
public int maxArea(int[] height) {
int ans = 0, left = 0, right = height.length - 1;
int maxArea = 0;
while(left < right){
//面积取决于指针对应的最小高度
maxArea = Math.min(height[left],height[right]) * (right - left);
ans = Math.max(ans,maxArea);
//移动 数字较小的那个指针
if(height[left] < height[right]){
left++;
}else{
right--;
}
}
return ans;
}
}
3. 链表
3.1 删除链表的倒数第 N 个结点
问题描述:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
思路
:先让fast跑n步,然后slow和fast再一起跑,fast到达末尾时,slow刚好为倒数第n+1个节点,下面代码使用了虚拟结点,所以跑n+1步。
1 -> 2 -> 3 -> 4 -> 5-> null
删除倒数第二个结点,即4;那么要使得3的指针指向5,即3 -> 5,在原链表上表现为 3.next = 3.next.next;
即删除结点的前一个结点 指向 该结点的后一个结点
采用双指针进行完成:快指针移动到n+1的位置后,慢指针开始移动
自测代码:
/**
* 删除链表的倒数第 N 个结点
* 输入:head = [1,2,3,4,5], n = 2
* 输出:[1,2,3,5]
*
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
public class RemoveNthFromEnd {
public static void main(String[] args) {
ListNode head = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4, new ListNode(5)))));
ListNode result = removeNthFromEnd(head,2);
ListNode curr = result;
while (curr != null) {
System.out.println(curr.val);
curr = curr.next;
}
}
/*
1 -> 2 -> 3 -> 4 -> 5-> null
删除倒数第二个结点,即4;那么要使得3的指针指向5,即3 -> 5,在原链表上表现为 3.next = 3.next.next;
即删除结点的前一个结点 指向 该结点的后一个结点
采用双指针进行完成:快指针移动到n+1的位置后,慢指针开始移动
*/
private static ListNode removeNthFromEnd(ListNode head, int n) {
//为避免要删除结点没有前一个结点,使用虚拟结点指向head
ListNode dummy = new ListNode(0,head);
ListNode fast = dummy;
ListNode slow = dummy;
//fast结点先移动到n节点的后一个结点位置
for (int i = 0; i <= n; i++) {
fast = fast.next;
}
//然后fast和slow结点一起移动,知道fast为null
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
// 此时 slow 的位置就是待删除元素的前一个位置
if(slow.next != null){
slow.next = slow.next.next;
}
return dummy.next;
}
力扣代码:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
//新建一个虚拟头节点指向head
ListNode dummy = new ListNode(0, head);
//快慢指针指向虚拟头节点
ListNode fast = dummy;
ListNode slow =dummy;
//快指针先移动n+1个节点,只要快慢指针相差 n 个结点即可
for(int i=0; i<=n;i++){
fast = fast.next;
}
//此时,快慢指针一起移动,快指针为null时,慢指针位于n的前一个节点
while(fast != null){
fast = fast.next;
slow = slow.next;
}
// 此时 slow 的位置就是待删除元素的前一个位置
if(slow.next != null){
slow.next = slow.next.next;
}
return dummy.next;
}
}