1、移除元素
- 题目:https://leetcode.cn/problems/remove-element/
- 思路:双指针,快慢双指针:快指针找到要移除的元素时快指针跳过,正常元素由快指针拷贝到慢指针位置
- 代码实现:
class Solution {
// 双指针--快慢指针
public int removeElement(int[] nums, int val) {
// 慢指针slow用于维护去掉val值后,数组存放的顺序索引遍历
// 快指针fast用于遍历nums中原来的值,fast遇到val跳过就行,不是val的放到slow的位置
int slow = 0;
for(int fast = 0; fast < nums.length; ++fast){
while(nums[fast] == val){
// 命中--fast直接跳过读取下一个应该保留的数
fast++;
if(fast == nums.length){
return slow;
}
}
// System.out.println("slow-->" + slow + ", fast-->" + fast);
nums[slow++] = nums[fast];
}
return slow;
}
}
2、反转字符串
- 题目:https://leetcode.cn/problems/reverse-string/
- 思路:双指针–首尾双指针,交换左右指针的元素;交换的实现既可以通过传统的设置临时变量,对于数值类型的交换还可以通过三次异或运算实现。
- 代码实现
class Solution {
// 双指针法--相向双指针
public void reverseString(char[] s) {
// 1、初始化双指针
int slow = 0;
int fast = s.length - 1;
while(slow < fast){
// 2、通过异或进行交换操作
s[slow] ^= s[fast];
s[fast] ^= s[slow];
s[slow] ^= s[fast];
// 3、指针移动
++slow;
--fast;
}
}
}
3、替换空格
- 题目:https://leetcode.cn/problems/ti-huan-kong-ge-lcof/
- 思路:双指针–快慢双指针:先统计空格个数,每有一个空格扩充两个字符位置;然后左右指针分别指向原字符串和扩容后的字符串的最后面,从后往前遍历(可以有效避免数组插入元素的移动)。
- 代码实现:
class Solution {
// 双指针法,为了数组的移动方便,无比从后往前
public String replaceSpace(String s) {
// 1、先统计空格的个数,对每个空格追加两个位置
StringBuilder sb = new StringBuilder(s);
for(int i = 0; i < s.length(); ++i){
if(s.charAt(i) == ' '){
sb.append(" ");
}
}
// 2、双指针启动
// 2.1、指针初始化
int left = s.length() - 1;
int fast = sb.length() - 1;
while(left >= 0){
// 2.2、核心逻辑--遇到空格补一个 "%20";正常字符的话就正常替换过来
if(s.charAt(left) == ' '){
sb.setCharAt(fast--, '0');
sb.setCharAt(fast--, '2');
sb.setCharAt(fast--, '%');
// 2.3、指针移动
--left;
}else{
// 不是空格,正常移动
sb.setCharAt(fast--, s.charAt(left--));
}
}
return new String(sb);
}
}
4、翻转字符串里的单词
- 题目:https://leetcode.cn/problems/reverse-words-in-a-string/
- 思路:先整体反转(相向双指针),每个单词局部反转(双指针)+移位
- 代码实现:
class Solution {
// 整体反转 + (每个单词局部反转 + 移位)
public String reverseWords(String s) {
// 1.单词整体反转
char[] ch = s.toCharArray();
reverseCharArray(ch, 0, s.length() - 1);
// 2.局部反转-每个单词
// i控制获得一个单词的起始结束索引
// j控制读取一个单词的字符但不读取空格
// k控制将每个单词放过来同时适当添加单词之间的一个空格
int j = 0, k = 0;
for(int i = 0; i < ch.length; ++i){
// 2.1 得到一个单词
if(ch[i] == ' '){
// 跳过空格
continue;
}
// 空格后的第一个字符,获得单词的起始索引
int wordStartIndex = i;
// 让i走完一个单词
while(i < ch.length && ch[i] != ' '){
++i;
}
int wordEndIndex = i - 1;
// 2.2 局部反转单词,然后用j来读取单词,然后通过k来放置单词
reverseCharArray(ch, wordStartIndex, wordEndIndex);
for(j = wordStartIndex; j <= wordEndIndex; ++j){
ch[k++] = ch[j];
}
// 2.3 每个单词结束追加空格,但注意避免越界(原字符串没有要删除的字符的情况)
if(k < ch.length){
ch[k++] = ' ';
}
}
// 3. 返回结果,k体现了当前结束位置,最后末尾可能有多出来的空格可能没有
// 没有的情况很好判断:k在原字符串末尾并且这个字符不为空格
return new String(ch, 0, (k == s.length()) && (ch[k - 1] != ' ') ? k : k - 1);
}
// 自定义字符串反转方法——左右双指针
public void reverseCharArray(char[] ch, int left, int right){
while(left < right){
// 反转操作
ch[left] ^= ch[right];
ch[right] ^= ch[left];
ch[left] ^= ch[right];
// 指针移动
++left;
--right;
}
}
}
5、翻转链表
- 题目:https://leetcode.cn/problems/reverse-linked-list/
- 思路:三指针–分别记录正序条件下的前一个元素、当前元素、下一个元素;然后通过让当前元素的 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 reverseList(ListNode head) {
// 1、指针初始化
ListNode prev = null;
ListNode cur = head;
while(cur != null){
// 2、核心逻辑--反转:从头开始倒着指
// 2.1、记录下cur的下一个,免得一会一倒就找不到了
ListNode temp = cur.next;
// 2.2、反转,让当前指向前一个
cur.next = prev;
// 3、指针移动
prev = cur;
cur = temp;
}
return prev;
}
}
6、删除链表的倒数第N个节点
- 题目:https://leetcode.cn/problems/remove-nth-node-from-end-of-list/
- 思路:双指针–快慢指针法,先让快指针走n步,然后快慢指针同步走,这样当快指针走到最后一个元素时,慢指针就走到了要删除的前一个节点。注意:链表中的删除操作要走到要删除节点的前一个,第一个节点也存在被删除的可能–添加虚拟节点。
- 代码实现:
/**
* 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 {
// 双指针,快指针先走n+1步,然后和慢指针一起走,当快指针走到末尾,慢指针就到了要删除节点的前一个
public ListNode removeNthFromEnd(ListNode head, int n) {
// 头结点可能被删除--设置虚拟头结点
// 1、指针初始化
ListNode dummyHead = new ListNode(-1);
dummyHead.next = head;
ListNode slow = dummyHead, fast = dummyHead;
for(int i = 0; i < n; ++i){
fast = fast.next;
}
// 2、指针移动
while(fast.next != null){
fast = fast.next;
slow = slow.next;
}
// 3、删除节点
slow.next = slow.next.next;
return dummyHead.next;
}
}
7、链表相交
- 题目:https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/
- 思路:双指针–相交点及其以后的节点一定是全部都对齐的,所以可以先让两个链表的尾部对齐–统计两个链表的长度,计算长度差,然后让长的链表指针先从头节点走长度差,这个时候长链表指针和位于短链表头部的指针对齐,逐个向后判断两个节点是否相等就可以了。
- 代码实现:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
// 双指针法
// 先求取两者长度,然后算得长度差,将长的那一个指针走长度差从而对齐两个链表的末端,逐个判断是否相交
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 1、计算两个链表的长度
int lenA = 0, lenB = 0;
ListNode nodeA = headA, nodeB = headB;
while(nodeA != null){
++lenA;
nodeA = nodeA.next;
}
while(nodeB != null){
++lenB;
nodeB = nodeB.next;
}
// 2、整理,将A设为长的链表,方便操作
if(lenA < lenB){
// 长度交换
int lenTemp = lenA;
lenA = lenB;
lenB = lenTemp;
// 链表交换
nodeA = headA;
headA = headB;
headB = nodeA;
}
// 3、移动长的指针使二者末尾对齐
int diff = lenA - lenB;
nodeA = headA;
nodeB = headB;
for(int i = 0; i < diff; ++i){
nodeA = nodeA.next;
}
// 4、逐个判断
while(nodeA != null){
if(nodeA == nodeB){
return nodeA;
}
nodeA = nodeA.next;
nodeB = nodeB.next;
}
return null;
}
}
8、环形链表Ⅱ
- 题目:https://leetcode.cn/problems/linked-list-cycle-ii/
- 思路:快慢双指针–快指针每次走两步,慢指针每次走一步,如果是环,必定相遇
- 1、记起点到环入口为x,环入口到相遇点长度为y,相遇点再到入口长度为z;
- 2、于是有 2(x + y) = x + (y + z) + y
- 3、得到 x = z
- 4、重点–所以在相遇后找到入口,只需要再从头开始一个指针,让原来的慢指针和新的指针同步一步一步走到相遇的地方就是入口!
- 代码实现:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
// 快慢双指针:快指针每次走两步,慢指针每次走一步,如果是环,必定相遇
// 1、记起点到环入口为x,环入口到相遇点长度为y,相遇点再到入口长度为z
// 2、于是有 2(x + y) = x + (y + z) + y
// 3、得到 x = z
// 4、所以在相遇后找到入口,只需要再从头开始一个慢指针,让原来慢指针和新的慢指针相遇就是入口!
public ListNode detectCycle(ListNode head) {
// 特殊情况判断
if(head == null){
return null;
}
// 1、快慢指针初始化
ListNode slow = head;
ListNode fast = head;
// 2、移动,直到两者相遇
while(fast.next != null && fast.next.next != null){
fast = fast.next.next;
slow = slow.next;
// System.out.println("fast-->" + fast + ", slow-->" + slow);
// 相遇
if(fast == slow){
// 3、再从头开始一个,和慢指针同步走到相遇--入口
// 直接使用fast作为新的从头开始的慢指针
fast = head;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
return null;
}
}
9、三数之和
- 题目:https://leetcode.cn/problems/3sum/
- 思路:第一个数通过遍历固定,然后后两个数的遍历通过相向双指针将时间复杂度由O(n^2)压缩到O(n);第一个数注意剪枝、去重,后两个数也注意去重。
- 代码实现:
class Solution {
// 双指针:固定第一个数,第二三个数用相向双指针--注意先排序,判断过程中注意去重
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ret = new ArrayList<>();
// 1、排序
Arrays.sort(nums);
// 2、遍历固定第一个数
for(int i = 0; i < nums.length; ++i){
// 2.1、第一个数剪枝
if(nums[i] > 0){
break;
}
// 2.2、第一个数去重
if(i > 0 && nums[i - 1] == nums[i]){
continue;
}
// 3、双指针获得后两个数
int left = i + 1, right = nums.length - 1, sum = 0;
while(left < right){
sum = nums[i] + nums[left] + nums[right];
if(sum > 0){
// 右指针的数大了
--right;
}else if(sum < 0){
// 左指针的数小了
++left;
}else{
// 这组结果是对的
List<Integer> list = new ArrayList<>();
list.add(nums[i]);
list.add(nums[left]);
list.add(nums[right]);
ret.add(list);
// 后两个数的去重
while(left < right && nums[right - 1] == nums[right]){
--right;
}
while(left < right && nums[left + 1] == nums[left]){
++left;
}
// 左右指针同时移动
++left;
--right;
}
}
}
return ret;
}
}
10、四数之和
- 题目:https://leetcode.cn/problems/4sum/
- 思路:三数之和进阶,遍历固定第一个数后,再遍历固定第二个数,最后剩下的两个数通过双指针压缩时间复杂度到O(n),前两个数注意剪枝、去重;最后两个数注意去重。
- 代码实现:
class Solution {
// 三数之和进阶:固定住前两个数,双指针获得后两个数,使后两个数的时间复杂度为O(n),降一阶
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> ret = new ArrayList<>();
// 1、数组排序
Arrays.sort(nums);
// 2、遍历固定第一个数
for(int i = 0; i < nums.length - 3; ++i){
// System.out.println("-------------");
// System.out.println("i-->" + i);
// 2.1 第一个数剪枝,注意target可能是负数
if(nums[i] > target && target > 0){
break;
}
// 2.2 第一个数去重
if(i > 0 && nums[i - 1] == nums[i]){
continue;
}
// 3、遍历固定第二个数
for(int j = i + 1; j < nums.length - 2; ++j){
// System.out.println("j-->" + j);
// 3.1、第二个数剪枝
if(nums[j] > target - nums[i] && nums[i] > 0){
break;
}
// 3.2、第二个数去重
if(j > i + 1 && nums[j - 1] == nums[j]){
continue;
}
// 4、双指针获得后两个数
int left = j + 1, right = nums.length - 1, sum = 0;
// System.out.println("left-->" + left);
// System.out.println("right-->" + right);
// 5、结果判断
while(left < right){
sum = nums[i] + nums[j] + nums[left] + nums[right];
if(sum > target){
// 右指针的数大了
--right;
}else if(sum < target){
// 左指针的数小了
++left;
}else{
// 结果对了
List<Integer> list = new ArrayList<>();
list.add(nums[i]);
list.add(nums[j]);
list.add(nums[left]);
list.add(nums[right]);
ret.add(list);
// 后两个数的去重
while(left < right && nums[right - 1] == nums[right]){
--right;
}
while(left < right && nums[left + 1] == nums[left]){
++left;
}
// 指针移动
++left;
--right;
}
}
}
}
return ret;
}
}