参考文章:https://programmercarl.com/
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
虚拟头结点
public ListNode removeElements(ListNode head, int val) {
ListNode dummyHead = new ListNode();
dummyHead.next = head;
ListNode pre = dummyHead;
while(pre.next!=null){
if(pre.next.val==val){
pre.next = pre.next.next;
}else{
pre = pre.next;
}
}
return dummyHead.next;
}
不使用虚拟头结点
public ListNode removeElements(ListNode head, int val) {
while(head!=null && head.val == val){
head = head.next;
}
ListNode pre = head;
while(pre!=null && pre.next!=null){
if(pre.next.val==val){
pre.next = pre.next.next;
}else{
pre = pre.next;
}
}
return head;
}
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev
以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
index
个节点的值。如果索引无效,则返回-1
。val
的节点。插入后,新节点将成为链表的第一个节点。val
的节点追加到链表的最后一个元素。index
个节点之前添加值为 val
的节点。如果 index
等于链表的长度,则该节点将附加到链表的末尾。如果 index
大于链表长度,则不会插入节点。如果index
小于0,则在头部插入节点。index
有效,则删除链表中的第 index
个节点。Tips:
1) 使用虚拟头结点
2)addAtHead 和 addAtTail 是 addAtIndex 的两种特殊情况
3)外部类可以使用内部类的私有成员变量
单向链表
class MyLinkedList {
class Node{
int val;
Node next;
public Node(){}
public Node(int val){
this.val = val;
}
}
int size;
Node dummyHead;
public MyLinkedList() {
dummyHead = new Node();
size = 0;
}
private Node getNode(int index){
Node cur = dummyHead;
while(index>-1){
cur = cur.next;
index--;
}
return cur;
}
public int get(int index) {
if(index<0 || index>=size){
return -1;
}
return getNode(index).val;
}
public void addAtHead(int val) {
addAtIndex(0,val);
}
public void addAtTail(int val) {
addAtIndex(size,val);
}
public void addAtIndex(int index, int val) {
if(index>size){
return;
}
Node node = new Node(val);
if(index<0){
index = 0;
}
Node pre = getNode(index-1);
node.next = pre.next;
pre.next = node;
size++;
}
public void deleteAtIndex(int index) {
if(index<0 || index>=size){
return;
}
Node pre = getNode(index-1);
pre.next = pre.next.next;
size--;
}
}
双向链表
1)不用虚拟尾结点
class MyLinkedList {
class Node{
int val;
Node next;
Node prev;
public Node(){}
public Node(int val){
this.val = val;
}
}
int size;
Node dummyHead;
public MyLinkedList() {
dummyHead = new Node();
size = 0;
}
private Node getNode(int index){
Node cur = dummyHead;
while(index>-1){
cur = cur.next;
index--;
}
return cur;
}
public int get(int index) {
if(index<0 || index>=size){
return -1;
}
return getNode(index).val;
}
public void addAtHead(int val) {
addAtIndex(0,val);
}
public void addAtTail(int val) {
addAtIndex(size,val);
}
public void addAtIndex(int index, int val) {
if(index>size){
return;
}
Node node = new Node(val);
if(index<0){
index = 0;
}
Node preNode = getNode(index-1);
node.next = preNode.next;
preNode.next = node;
node.prev = preNode;
if(index !=0 && index !=size){
node.next.prev = node;
}
size++;
}
public void deleteAtIndex(int index) {
if(index<0 || index>=size){
return;
}
Node preNode = getNode(index-1);
preNode.next = preNode.next.next;
if(index != size-1){
preNode.next.prev = preNode;
}
size--;
}
}
2)使用虚拟尾结点
class MyLinkedList {
class Node{
int val;
Node next;
Node prev;
public Node(){}
public Node(int val){
this.val = val;
}
}
int size;
Node dummyHead, dummyTail;
public MyLinkedList() {
dummyHead = new Node();
dummyTail = new Node();
dummyHead.next = dummyTail;
dummyTail.prev = dummyHead;
size = 0;
}
private Node getNode(int index){
if(index==-1){
return dummyHead;
}
Node cur = dummyHead.next;
if(index<size/2){
while(index>0){
cur = cur.next;
index--;
}
}else{
cur = dummyTail;
index = size-index;
while(index>0){
cur = cur.prev;
index--;
}
}
return cur;
}
public int get(int index) {
if(index<0 || index>=size){
return -1;
}
return getNode(index).val;
}
public void addAtHead(int val) {
addAtIndex(0,val);
}
public void addAtTail(int val) {
addAtIndex(size,val);
}
public void addAtIndex(int index, int val) {
if(index>size){
return;
}
Node node = new Node(val);
if(index<0){
index = 0;
}
Node preNode = getNode(index-1);
node.next = preNode.next;
node.next.prev = node;
preNode.next = node;
node.prev = preNode;
size++;
}
public void deleteAtIndex(int index) {
if(index<0 || index>=size){
return;
}
Node preNode = getNode(index-1);
preNode.next = preNode.next.next;
preNode.next.prev = preNode;
size--;
}
}
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
双指针法
public ListNode reverseList(ListNode head) {
ListNode pre = null;
while(head!=null){
ListNode nextNode = head.next;
head.next = pre;
pre = head;
head = nextNode;
}
return pre;
}
递归法
每次输入一个头结点就能返回该链表反转后新的头结点
1)终止条件:链表为空或链表只有一个头结点时,返回
2)过程:除head结点外,后边的链表已经反转好,此时链表的最后一个结点就是head.next,再将head结点添加至反转好链表末尾即可,即令 head.next.next = head, 尾结点指向空head.next = null;
public ListNode reverseList(ListNode head) {
if(head==null || head.next == null){
return head;
}
ListNode rHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return rHead;
}
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
虚拟头结点
public ListNode swapPairs(ListNode head) {
ListNode dummyHead = new ListNode();
dummyHead.next = head;
ListNode pre = dummyHead;
while(pre.next!=null && pre.next.next!=null){
ListNode one = pre.next;
ListNode two = one.next;
one.next = two.next;
two.next = one;
pre.next = two;
pre = pre.next.next;
}
return dummyHead.next;
}
递归
public ListNode swapPairs(ListNode head) {
if(head==null || head.next==null){
return head;
}
ListNode rHead = swapPairs(head.next.next);
ListNode two = head.next;
head.next = rHead;
two.next = head;
return two;
}
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
快慢指针
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummyHead = new ListNode();
dummyHead.next = head;
ListNode slowNode = dummyHead;
ListNode fastNode = slowNode;
while(n>=0){
fastNode = fastNode.next;
n--;
}
while(fastNode!=null){
slowNode = slowNode.next;
fastNode = fastNode.next;
}
slowNode.next = slowNode.next.next;
return dummyHead.next;
}
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。
利用 headA.len + headB.len = headB.len + head.len
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pointA = headA;
ListNode pointB = headB;
boolean changA = false, changB = false;
while(pointA!=null && pointB!=null){
if(pointA == pointB){
return pointA;
}
if(pointA.next==null && !changA){
pointA = headB;
changA = true;
}else{
pointA = pointA.next;
}
if(pointB.next==null && !changB){
pointB = headA;
changB = true;
}else{
pointB = pointB.next;
}
}
return null;
}
}
一些优化,headA.len+1 + headB.len = headB.len +1+ head.len (1代表链表结尾的null)
当两个指针同时为空说明已经两链表中无相交元素(同时为空,要么两链表长度相等,要么已经两个指针均已遍历完两个链表中所有结点)
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pointA = headA;
ListNode pointB = headB;
while(pointA!=pointB){
pointA = pointA!=null?pointA.next:headB;
pointB = pointB!=null?pointB.next:headA;
}
return pointA;
}
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
解题思路:
1)用快慢指针判断是否有环:若相遇,则有环;若快指针走到null,无环
2)在有环的情况下确定环的入口:利用2(x+y) = x+y + n(y+z) 可推得 x = (n-1)(y+z) + z
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s9Z6SFnF-1667716332451)(img/image-20221104193334649.png)]
public ListNode detectCycle(ListNode head) {
// 1. 利用快慢指针判断有无环
ListNode slow = head;
ListNode fast = head;
while(fast!=null && fast.next!=null){
slow = slow.next;
fast = fast.next.next;
if(slow==fast){
break;
}
}
if(fast==null || fast.next==null){
return null;
}
// fast == slow 相遇,有环
// 2. 在有环的情况下确定环的入口
ListNode index = head;
while(fast!=index){
fast = fast.next;
index = index.next;
}
return index;
}
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
给定两个字符串 *s*
和 *t*
,编写一个函数来判断 *t*
是否是 *s*
的字母异位词。
**注意:**若 *s*
和 *t*
中每个字符出现的次数都相同,则称 *s*
和 *t*
互为字母异位词。
public boolean isAnagram(String s, String t) {
if(s.length() != t.length()) return false;
Map<Character, Integer> map = new HashMap<>();
for(int i=0;i<s.length();i++){
char cs = s.charAt(i);
if(map.containsKey(cs)) map.put(cs,map.get(cs)+1);
else map.put(cs,1);
char ct = t.charAt(i);
if(map.containsKey(ct)) map.put(ct,map.get(ct)-1);
else map.put(ct,-1);
}
for(Integer i: map.values()){
if(i!=0){
return false;
}
}
return true;
}
由于小写字母是有限的26个,因此可以用定长数组代替map。数组的index相当于key,arr[index]相当于value。数组的定位和值修改起来更加简便。
使用数组来做哈希的题目,是因为题目都限制了数值的大小
public boolean isAnagram(String s, String t) {
if(s.length() != t.length()) return false;
int[] arr = new int[26];
for(int i=0;i<s.length();i++){
char cs = s.charAt(i);
arr[cs-'a'] += 1;
char ct = t.charAt(i);
arr[ct-'a'] -= 1;
}
for(Integer i: arr){
if(i!=0){
return false;
}
}
return true;
}
给定两个数组 nums1
和 nums2
,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
**Tips:**Set转Array
//将结果几何转为数组
res.stream().mapToInt(x -> x).toArray();
具体实现:
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> res = new HashSet<>();
Set<Integer> set = new HashSet<>();
for(int a : nums1){
set.add(a);
}
for(int b: nums2){
if(set.contains(b)){
res.add(b);
}
}
int[] arr = new int[res.size()];
int i = 0;
for(Integer num : res){
arr[i++] = num;
}
return arr;
}
编写一个算法来判断一个数 n
是不是快乐数。
「快乐数」 定义为:
如果 n
是 快乐数 就返回 true
;不是,则返回 false
。
public boolean isHappy(int n) {
Set<Integer> set = new HashSet<>();
while(n!=1){
int sum = 0;
while(n != 0){
sum += (int)Math.pow(n%10,2);
n /= 10;
}
if(set.contains(sum)){
return false;
}
set.add(sum);
n = sum;
}
return true;
}
一些代码优化
class Solution {
public boolean isHappy(int n) {
Set<Integer> record = new HashSet<>();
while (n != 1 && !record.contains(n)) {
record.add(n);
n = getNextNumber(n);
}
return n == 1;
}
private int getNextNumber(int n) {
int res = 0;
while (n > 0) {
int temp = n % 10;
res += temp * temp;
n = n / 10;
}
return res;
}
}
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
思路:想明白
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
Map<Integer,Integer> map = new HashMap<>();
for(int i=0;i<nums.length;i++){
int temp = target-nums[i];
if(map.containsKey(temp)){
res[0] = map.get(temp);
res[1] = i;
break;
}
map.put(nums[i],i);
}
return res;
}
给你四个整数数组 nums1
、nums2
、nums3
和 nums4
,数组长度都是 n
,请你计算有多少个元组 (i, j, k, l)
能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
思路:
暴力解法需要四层循环遍历四个数组求出不同组合的和,时间复杂度为O(n^4)。
为减少时间复杂度,我们先求出nums1和nums2的不同组合之和,放入map中(key:sum;value:和为sum的不同组合个数),时间复杂度为O(n^2)。再求出 nums3和nums4 的sum,看map中是否存在一个key == -(nums3[k]+nums[l])。
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
int n = nums1.length;
Map<Integer,Integer> map = new HashMap<>();
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
int sum = nums1[i]+nums2[j];
if(map.containsKey(sum)) map.put(sum,map.get(sum)+1);
else map.put(sum,1);
}
}
int count = 0;
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
int sum = -(nums3[i]+nums4[j]);
if(map.containsKey(sum)) count += map.get(sum);
}
}
return count;
}
给你两个字符串:ransomNote
和 magazine
,判断 ransomNote
能不能由 magazine
里面的字符构成。
如果可以,返回 true
;否则返回 false
。
magazine
中的每个字符只能在 ransomNote
中使用一次。
思路:
因为题目所只有小写字母,那可以采用空间换取时间的哈希策略, 用一个长度为26的数组还记录magazine里字母出现的次数。
然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。
在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!
public boolean canConstruct(String ransomNote, String magazine) {
if(ransomNote.length()>magazine.length()){
return false;
}
int[] arr = new int[26];
for(char c : magazine.toCharArray()){
arr[c-'a'] += 1;
}
for(char c : ransomNote.toCharArray()){
arr[c-'a'] -= 1;
if(arr[c-'a']<0){
return false;
}
}
return true;
}
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
**注意:**答案中不可以包含重复的三元组。
双指针法:
一个指针固定的情况下,两个指针不断变化找结果
i:指向第一个数字 left:指向第二个数字 right:指向第三个数字
目的是令 nums[i] + nums[left] + nums[right] == 0
1)对数组进行排序
2)令i指针遍历整个数组
3)i固定,即第一个数固定时,找第二第三个数:从 left=i+1 和 right=nums.length-1 开始判断nums[i] + nums[left] + nums[right],并逐渐缩小[left,right]的范围,直到 left>=right为止
nums[i] + nums[left] + nums[right] < 0,left指针右移
nums[i] + nums[left] + nums[right] > 0,right指针左移
nums[i] + nums[left] + nums[right] == 0,left 和 right指针同时向对方移动(继续考虑其他组合情况)
**Tips:**将数组变为动态数组的方法
Arrays.asList(nums[i],nums[left++],nums[right--]);
具体实现:
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
// 由于仅需要三个数字和为0,而不关心每个数字对应的下标,因此数组内元素位置可以改变,支持排序
Arrays.sort(nums);
for(int i=0;i<nums.length;i++){
// 1)剪枝:
// 由于nums已经是一个排序数组了,且left和right指针在i指针之后,因此若nums[i]>0,则 nums[left]>0, nums[right]>0, 从i开始到数组结束的组合中不存在答案
if(nums[i]>0){
return res;
}
// 2)给nums[i]去重
// 不可用nums[i]==nums[i+1]条件是因为:指针在i情况和i+1情况都没有考虑过,且i+1包含在left遍历的范围中,满足条件的结果数组内元素可以重复(即第一二三个数可以重复),如[-1,-1,2]。若此时去重,去的是结果数组内部的重
// 用nums[i]==nums[i-1]条件的原因: 由于i-1在i左边,已经考虑过第一个数是nums[i-1]的情况,因此若nums[i]==nums[i-1],可以跳过
if(i>0 && nums[i]==nums[i-1]){
continue;
}
int left = i+1;
int right = nums.length-1;
while(left<right){
int sum = nums[i] + nums[left] + nums[right];
if(sum<0){
left++;
}else if(sum>0){
right--;
}else{
// sum==0
// 3)nums[left] 和 nums[right]去重:若left和right与已经处理过的情况相同,就跳过。对left来说left-1已经处理过,对right来说right+1已经处理过。
res.add(Arrays.asList(nums[i],nums[left++],nums[right--]));
while(left<right && nums[left] == nums[left-1]){
left++;
}
while(left<right && nums[right] == nums[right+1]){
right--;
}
}
}
}
return res;
}
给你一个由 n
个整数组成的数组 nums
,和一个目标值 target
。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a
、b
、c
和 d
互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
**双指针法:**思路和三数之和相同,仅剪枝情况不同
两个指针固定的情况下,两个指针不断变化找结果
i:指向第一个数字 j:指向第一个数字 left:指向第二个数字 right:指向第三个数字
目的是令 nums[i] + nums[j] + nums[left] + nums[right] == target
由于target可能为负,而负数相加,越加越小,因此要分情况剪枝
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
for(int i=0;i<nums.length;i++){
// 剪枝
// 注意:target可能为负,此时两个比target大的负数相加有可能等于target
if((target<0 && nums[i]>=0)||(target>=0 && nums[i]>target)){
break;
}
// 去重
if(i>0 && nums[i]==nums[i-1]){
continue;
}
for(int j=i+1;j<nums.length;j++){
// 2级剪枝
if((target<0 && nums[i]+nums[j]>=0) || (target>=0 && nums[i]+nums[j]>target)){
break;
}
// 去重
if(j>i+1 && nums[j]==nums[j-1]){
continue;
}
int left = j+1;
int right = nums.length-1;
while(left<right){
long sum = nums[i] + nums[j] + nums[left] + nums[right];
if(sum>target){
right--;
}else if(sum<target){
left++;
}else{
res.add(Arrays.asList(nums[i],nums[j],nums[left++],nums[right--]));
while(left<right && nums[left]==nums[left-1]){
left++;
}
while(left<right && nums[right]==nums[right+1]){
right--;
}
}
}
}
}
return res;
}