在做顺序表和链表的相关OJ题时,很多类似题的解法也比较类似,为了能更好的复习回顾,特别记录一下在做题时的一些思路历程。
顺序表
删除有序数组中的重复项
1、描述:给你一个升序排列的数组 nums
,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。元素的相对顺序应该保持 一致 。
2、思路: 使得每个元素只出现一次?这要怎样做,有什么方法来判定元素出现的次数?使用双指针。原地删除?也就是说不能引入额外的空间,需要在原数组上操作。定义两个指针fir
和sec
,初始都在索引为0的位置,何时需要移动这两个指针呢?看条件,原地删除使得每个元素只出现一次,那就先定义[0,fir]
这个区间上的所有元素都是只出现了一次的元素,不断向后移动sec,当此时nums[fir] != nums[sec]
时,也就是说此时这两个位置的元素是不重复的,我们再移动fir
指针,然后用sec
位置的值覆盖fir位置的值,继续保证[0,fir]这个区间上的所有元素始终只出现了一次,直到sec走到null
,循环结束,最终得到的[0,fir]
之间的所有元素都是满足题意的,新数组的长度就是fir + 1
。
3、代码实现
public class Num_26_RemoveDuplicates {
public int removeDuplicates(int[] nums) {
int fir = 0, sec = 0;
while (sec< nums.length){
if(nums[fir]!=nums[sec]){
fir++;
nums[fir] = nums[sec];
}
//更新循环条件
sec ++;
}
return fir+1;
}
}
移除元素
1、描述: 给你一个数组 nums
和一个值 val
,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。元素的顺序可以改变。不需要考虑数组中超出新长度后面的元素。
2、思路: 乍一看这个题和26号题类似,等同于原地移除所有数值等于 val
的元素,让它一次都不出现。做法是不是和26号问题一致呢?其实思路基本不变。采用双指针fir
,sec
。让sec指针不断向后遍历,还是那个问题,什么时候要移动fir指针呢?如果nums[sec] != val
,要先用nums[sec]
的值覆盖当前nums[fir]
的值,然后再移动fir
指针。因为如果先移动fir,那fir原始为位置可能就是重复元素。需要先覆盖,让它变为不重复元素,然后再移动fir
。那还是定义[0,fir]
之间的所有元素都是不重复的?这里有点细微的差别,fir
位置上的元素可能就是重复出现的元素,所以不重复的区间应该是[0,fir)
,删除完之后数组的长度就是fir
。
3、代码实现
public class Num_27_RemoveValue {
public int removeElement(int[] nums, int val) {
int fir = 0, sec = 0;
//nums[0...fir)一定是数组中值不为val的区间
while(sec< nums.length){
if (nums[sec]!=val){
nums[fir]=nums[sec];
fir ++;
}
sec ++;
}
return fir;
}
}
删除有序数组中的重复项II
1、描述: 给你一个有序数组 nums
,请你 原地删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。不要使用额外的数组空间,你必须在原地修改输入数组 并在使用 O(1) 额外空间的条件下完成。
2、思路:题目要求每个元素最多出现k
次,这种问题和26、27是一类问题,核心就是对于指针运动条件的考量,什么时候需要移动index
指针?确定指针运动条件,根据index
的定义:不重复的元素的右边界,那么当有新的不重复的元素需要加入时index
将向右移动。 如何运动呢?这仍然跟指针的定义有关系,用循环不变量来说:就是你得在循环的时候保持变量的定义不变。 因为这里我们定义不重复元素集合是[0,index]左闭右闭这是循环不变量,因此index
这个下标是可以用来放新的不重复元素的。同时index++
;保持下一个循环时任然是[0,index]
左闭右闭。nums[i] != nums[index]
表示当前遍历的位置i和不重复集合的右边界不一样,那么肯定能放入不重复集合了,题目要求每个元素最多出现k次,那么当nums[i] != nums[index-(k-1)]
,最多允许到index-(k-1)
的元素与index
相同,也就是i
最极限的情况与index-(k-1)
相同,就可以保证不会存在连续的重复元素超过k
个。
3、代码实现
public int removeDuplicates(int[] nums) {
//定义元素重复的最大次数
int k = 2;
// 1.定义[0,index] 是修改后的满足要求的数组区间,这里已经把0,1,2 ...k- 1 ,共k个数放进去了
int index = k - 1;
// 2.判断终止条件
for(int i = k;i < nums.length;i++){
// 3.指针移动条件
if(nums[i] != nums[index-k+1]){
index++;
nums[index] = nums[i];
}
}
// 4.判断返回值
return index + 1;
}
}
合并两个有序数组
1、描述: 给你两个按 非递减顺序 排列的整数数组 nums1
和 nums2
,另有两个整数 m
和 n
,分别表示 nums1
和 nums2
中的元素数目。请你 合并 nums2
到 nums1
中,使合并后的数组同样按非递减顺序排列。
2、思路: 先遍历两个子数组,题目要求合并后的数组元素要按非递减顺序排雷,所以通过比较子数组中元素的大小关系来决定tmp
数组中元素的值,遍历完之后,将剩下的元素写回tmp
数组,由于题目要求是要将nums2
合并到nums1
中,所以最终再将tmp
数组中的所有元素写回nums1
。
3、代码实现:
public class Num_88_MergeTwoArray {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = 0,j = 0,k = 0;
int[] tmp = new int[nums1.length];
//此时两个子数组都还有元素没遍历完
while(i<m && j<n){
if(nums1[i]<nums2[j]){
//将i对应的元素写回tmp
tmp[k ++] = nums1[i ++];
}else{
tmp[k ++] = nums2[j ++];
}
}
//此时一定有一个数组遍历完了
// 只需要将剩下还没走完的写回tmp
while(i<m){
tmp[k++] = nums1[i++];
}
while(j<n){
tmp[k++] = nums2[j++];
}
//此时两个数组的所有有效元素全部合并在了tmp数组中,写回nums1
for (int l = 0; l < nums1.length; l++) {
nums1[l] = tmp[l];
}
}
}
消失的数字
1、描述: 数组nums
包含从0到n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。
2、代码实现
public class Number17_04_Missing_Number {
//1、排序法
public int missingNumber(int[] nums) {
//先将数组转为有序数组
Arrays.sort(nums);
//遍历整个数组
for (int i = 0; i < nums.length; i++) {
//若当前索引位置的元素是和索引值不同
if(nums[i]!=i){
//则当前索引值就是消失的数字,直接返回
return i;
}
}
//此时消失的数字就是最后一个数字
return nums.length;
}
//2、哈希思想
public int missingNumber(int[] nums) {
//创建一个布尔数组
boolean[] array = new boolean[nums.length + 1];
//遍历原数组,将出现的值当成布尔数组的索引,并将其置为true
for (int i = 0; i < nums.length; i++) {
array[nums[i]] = true;
}
//遍历布尔数组,此时值为false位置对应的索引即为消失的数字
for (int i = 0; i < array.length; i++) {
if(!array[i]){
return i;
}
}
return -1;
}
//3、异或思想
public int missingNumber(int[] nums) {
int ret = 0;
//遍历整个数组
for (int i = 0; i < nums.length; i++) {
//将数组下标和数组的值异或
ret^=i;
ret^=nums[i];
}
//与数组的长度异或
ret^=nums.length;
//相同的值异或为0,异或完的结果就是消失的数字
return ret;
}
}
轮转数组
1、描述: 给定一个整数数组 nums
,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
2、思路: 这个题的核心在于k只要求了非负数,如果k大于了数组长度,该如何表示。其实i+k
超出数组长度,移位数等效于(i+k)%nums.length
。
3、代码实现
public void rotate(int[] nums, int k) {
//开辟一个新数组,和元素组长度一样
int[] array = new int[nums.length];
//遍历原数组,将原数组中的每个元素右移k位之后放入新数组
//i+k超出数组长度,移位数等效于(i+k)%nums.length
for (int i = 0; i < nums.length; i++) {
array[(k+i)%nums.length] = nums[i];
}
//将得到的新数组覆盖掉原数组
for (int i = 0; i < nums.length; i++) {
nums[i] = array[i];
}
}
链表
删除排序链表中的重复元素
1、描述: 给定一个已排序的链表的头 head
, 删除所有重复的元素,使每个元素只出现一次 。返回已排序的链表 。
2、思路: 这个题目和顺序表部分的第一个题目很类似,在这里我们也可以借助双引用的思想,但更简洁易懂的方法是递归法,先把子链表中所有重复的节点递归的删除,只保留一次,最后再判断头节点是不是待删除的节点。在使用递归方法的过程中,重点在于边界条件的把握。
3、代码实现
public ListNode deleteDuplicates(ListNode head) {
//base case
if(head==null || head.next==null){
return head;
}
//链表一定至少2个节点
ListNode dummyhead = new ListNode(-101);
dummyhead.next = head;
ListNode prev = dummyhead;
ListNode cur = prev.next;
while (cur!=null){
if(prev.val == cur.val){
prev.next = cur.next;
}else{
//只有当两个值不相等时才能移动prev的指向
prev = prev.next;
}
//不管重复与否,要更新cur
cur = cur.next;
}
return dummyhead.next;
}
//递归法
public ListNode deleteDuplicates(ListNode head) {
//base case
if(head == null || head.next == null){
return head;
}
//先把子链表中所有重复节点删除,只保留一次
head.next = deleteDuplicates(head.next);
//再判断头节点是否是重复节点
if(head.val == head.next.val){
return head.next;
}
return head;
}
}
删除排序链表中的重复元素II
1、描述: 给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回已排序的链表 。
2、思路: 采用递归的思想。
3、代码实现
//方法一
public ListNode deleteDuplicates(ListNode head) {
//base case
if(head == null || head.next == null){
return head;
}
//定义虚拟头节点
ListNode dummyhead = new ListNode(-101);
//prev一定要指向不是重复的节点
dummyhead.next = head;
ListNode prev = dummyhead;
ListNode cur = prev.next;
while (cur!=null){
ListNode sec = cur.next;
if(sec == null){
break;
}
if(cur.val!= sec.val){
//只有当cur和sec的值不相等时才能移动prev
prev = prev.next;
}else{
//此时cur和sec相等
while(sec!=null && cur.val == sec.val){
//此时移动sec引用直到cur的值和sec的值不同
sec = sec.next;
}
//此时sec一定走到第一个和cur不相等的节点
//prev。。。sec之间全都是待删除的节点
}
//更新cur,指向sec
cur = sec;
}
return dummyhead.next;
}
//方法二
//递归实现
//传入一个以head为头节点的链表,就能删除其中所有的重复元素,重复元素一个都不保留
public ListNode deleteDuplicates(ListNode head) {
//base case
if(head == null || head.next == null){
return head;
}
if(head.val!=head.next.val){
head.next = deleteDuplicates(head.next);
//此时head本身不会重复
return head;
}else{
//head节点是重复节点
//先处理完头节点的情况
ListNode newHead = head.next;
while (newHead!=null && newHead.val== head.val){
//将newHead移动到不是重复节点的位置
newHead = newHead.next;
}
//走出循环,newHead一定不是待删除节点,最终传入整个函数
return deleteDuplicates(newHead);
}
}
移除链表元素
1、描述: 给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回新的头节点 。
2、思路: 采用递归的思想,先把以head.next
为头节点的子链表中所有值为val
的删除完毕,最后处理头节点。
3、代码实现:
//1、虚拟头节点法
public ListNode removeElements(ListNode head, int val) {
//base case
if(head == null){
return null;
}
//定义虚拟头节点
ListNode dummyhead = new ListNode(val);
//将虚拟头节点和原链表建立联系
dummyhead.next = head;
//prev一定指向值不为val的节点
ListNode prev = dummyhead;
while(prev.next!=null){
if(prev.next.val == val){
prev.next = prev.next.next;
}else{
//后一个节点不是要删除的节点,prev继续向后移动
prev = prev.next;
}
}
return dummyhead.next;
}
//2、递归实现
public ListNode removeElements(ListNode head, int val) {
//base case
if(head == null){
return null;
}
//先把以head.next为头节点的子链表中所有值为val的删除完毕
head.next = removeElements(head.next,val);
//最后处理头节点是否需要删除即可
return head.val==val?head.next:head;
}
合并两个有序链表
1、描述: 将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
2、思路: 仍然是递归的思想。
3、代码实现
//方法一 虚拟头节点法
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
//base case
if(list1 == null){
return list2;
}
if(list2 == null){
return list1;
}
ListNode dummyhead = new ListNode();
//定义尾节点
ListNode tail = dummyhead;
//当list1和list2都不为空时,取较小值拼接在dummyhead之后
while(list1 != null && list2 != null){
if(list1.val <= list2.val){
tail.next = list1;
//更新tail
tail = list1;
//list1继续向下走
list1 = list1.next;
}else {
tail.next = list2;
tail = list2;
list2 = list2.next;
}
}
//循环结束之后至少有一个链表为空,此时拼接不为空的链表即可
if(list1 == null){
tail.next = list2;
}
if(list2 == null){
tail.next = list1;
}
return dummyhead.next;
}
//方法二
//递归实现
//传入两个以list1和list2为头节点的链表,就能合并两个链表并返回新链表的头节点
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
//base case
if(list1 == null){
return list2;
}
if(list2 == null){
return list1;
}
if(list1.val <= list2.val){
//合并list1从第二个节点开始的链表和整个list2,并拼接在list1头节点的后面
list1.next= mergeTwoLists(list1.next,list2);
return list1;
}else {
list2.next = mergeTwoLists(list1, list2.next);
return list2;
}
}
链表的中间节点
1、描述: 给你单链表的头结点 head
,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
2、思路: 快慢指针。让慢指针一次走一步,快指针一次走两步,循环结束之后慢指针恰好在中间位置。
3、代码实现
//方法一
public ListNode middleNode(ListNode head) {
if(head==null || head.next == null){
return head;
}
int length = 0;
ListNode newhead = head;
while(newhead!=null){
length++;
newhead = newhead.next;
}
for (int i = 0; i < length/2; i++) {
head = head.next;
}
return head;
}
//方法2;快慢指针
public ListNode middleNode(ListNode head) {
ListNode low = head,fast = head;
while (fast!=null && fast.next!=null){
//让慢指针一次走一步,快指针一次走两步
low = low.next;
fast = fast.next.next;
}
//循环结束,此时low恰好在中间位置
return low;
}
链表的倒数第K个节点
1、描述: 输入一个链表,输出该链表中倒数第k个结点。
2、思路: 快慢指针。让快指针先走k步,然后同时移动快指针和慢指针,当快指针指向空时,此时慢指针就是所求的节点。
3、代码实现
public ListNode FindKthToTail(ListNode head,int k) {
//base case
if(head == null || k<=0){
return null;
}
ListNode fast = head, low = head;
//先让fast走k步
for (int i = 0; i < k; i++) {
if(fast==null){
return null;
}
fast = fast.next;
}
//同时移动fast和low,当fast为空时,此时low即为所求节点
while (fast != null){
fast = fast.next;
low = low.next;
}
return low;
}
分隔链表
1、描述: 给你一个链表的头节点 head
和一个特定值 x
,对链表进行分隔,使得所有 小于 x
的节点都出现在 大于或等于 x
的节点之前。保留两个分区中每个节点的初始相对位置。
2、思路: 这个题的做法和我们的直观做法一样,唯一需要注意的是,当我们根据元素大小关系将原链表的元素拆分开,然后将小链表的尾部和大链表的头部拼接时,此时大链表的尾部可能还有链接,所以需要断开此连接。
3、代码实现
public ListNode partition(ListNode head, int x) {
//base case
if(head == null || head.next == null){
return head;
}
//定义两个链表
ListNode smallhead = new ListNode(), bighead = new ListNode();
ListNode smalltail = smallhead,bigtail = bighead;
//遍历原链表,比较与x的大小关系,将小于x的节点拼接在smallhead之后,>=x的节点拼接在bighead之后
while (head != null){
if(head.val < x){
smalltail.next = head;
smalltail = head;
head = head.next;
}else {
bigtail.next = head;
bigtail = head;
head = head.next;
}
}
//循环结束将小链表的尾部与大链表的头部拼接
//注意,此时大链表的尾部可能还有连接,需要断开此连接
smalltail.next = bighead.next;
bigtail.next = null;
return smallhead.next;
}
反转链表
1、描述:给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
2、思路: 这个题目的方法有很多种,可以用虚拟头节点法,也可以双引用法,或者是简洁明了的递归法。
3、代码实现
//方法一 虚拟头节点
public ListNode reverseList(ListNode head) {
//base case
if(head == null || head.next == null){
return head;
}
//定义虚拟头节点
ListNode dummyhead = new ListNode();
//不断遍历原链表,产生新节点,头插到新链表
while (head!=null){
ListNode node = new ListNode(head.val);
node.next = dummyhead.next;
dummyhead.next = node;
head = head.next;
}
return dummyhead.next;
}
//方法二
public ListNode reverseList(ListNode head) {
//base case
if(head == null || head.next == null){
return head;
}
//定义两个引用
ListNode prev = null;
ListNode cur = head;
while(cur!=null){
//先暂存一下cur位置的下一个节点的地址
ListNode next = cur.next;
//将原链表中的prev->cur变为cur->prev
cur.next = prev;
prev = cur;
cur = next;
}
//此时新链表恰好是以prev为头节点的链表
return prev;
}
//方法3、递归
public ListNode reverseList(ListNode head) {
//base case
if(head == null || head.next == null){
return head;
}
//先暂存一下头节点的下一个节点,防止转完之后丢失
ListNode next = head.next;
//反转以head.next为头节点的子链表,返回反转后链表的头节点
ListNode newhead = reverseList(head.next);
//将原链表的头节点与反转后的子链表拼起来
head.next= null;
next.next= head;
return newhead;
}
相交链表
1、描述: 给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
2、思路: 这个题非常有意思,引入双引用,核心问题在于判断他们何时相较。其实这是一个数学问题。在遍历两个链表的过程中,我们让L1走完自己本身之后倒过头来走L2,同样让L2走完自己本身之后倒过头来走L1,数学上可表示为:X + Y + Z = Z + Y + X,恒成立。
3、代码实现
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//引入两个引用l1和l2,同时开始遍历
//让l1走完自己的路之后倒过来走一遍l2,x + z + y = y + z + x
ListNode l1 = headA,l2 = headB;
while (l1!=l2){
l1 = l1 == null?headB:l1.next;
l2 = l2 == null?headA:l2.next;
}
return l1;
}
判断回文链表
1、描述: 给你一个单链表的头节点 head
,请你判断该链表是否为回文链表。如果是,返回 true
;否则,返回 false
。
**2、思路:**这个题目主要用到前面的找链表的中间节点方法和反转链表的方法。先找到链表的中间节点,然后将后半部分链表反转,与前半段链表进行比较,一旦有元素值不相同,即为false。
3、代码实现
public boolean isPalindrome(ListNode head) {
//base case
if(head==null && head.next == null){
return true;
}
//先找到链表的中间节点
ListNode middlenode = middleNode(head);
//将后半段链表反转
ListNode l2 = reverseList(middlenode);
while(head!=null && l2 != null){
if(head.val != l2.val){
return false;
}
head = head.next;
l2 = l2.next;
}
return true;
}
public ListNode middleNode(ListNode head) {
ListNode low = head,fast = head;
while (fast!=null && fast.next!=null){
//让慢指针一次走一步,快指针一次走两步
low = low.next;
fast = fast.next.next;
}
//循环结束,此时low恰好在中间位置
return low;
}
public ListNode reverseList(ListNode head) {
//base case
if(head == null || head.next == null){
return head;
}
//先暂存一下头节点的下一个节点,防止转完之后丢失
ListNode next = head.next;
//反转以head.next为头节点的子链表,返回反转后链表的头节点
ListNode newhead = reverseList(head.next);
//将原链表的头节点与反转后的子链表拼起来
head.next= null;
next.next= head;
return newhead;
}
判断环形链表
1、描述: 给你一个链表的头节点 head
,判断链表中是否有环。如果链表中存在环 ,则返回 true 。 否则,返回 false
。
2、思路: 这道题也是一道数学题,重点在于寻找链表有环的条件。采用快慢指针,遍历整个链表,让快指针每次走两步,慢指针每次走一步,若链表有环,总会在某一个时刻相遇。
3、代码实现
public boolean hasCycle(ListNode head) {
//快慢指针
ListNode fast = head,low = head;
//遍历整个链表,让快指针每次走两步,慢指针每次走一步,若链表有环,总会相遇
while (fast!= null && fast.next!= null){
low = low.next;
fast = fast.next.next;
if(low == fast){
return true;
}
}
//循环结束还没有找到
return false;
}
环形链表II
1、描述: 给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。不允许修改链表。
2、思路: 双指针思想。这个题的核心在于环形链表环的入口的寻找方法,本质也是一个数学上的问题。通过数学证明,可以得到一个结论:当快慢指针重合时,引入一个新的节点从链表的头节点开始走,最终新节点和慢指针一定会在环的入口相遇。
3、代码实现
public ListNode detectCycle(ListNode head) {
//结论:当快慢指针重合时,引入一个新的节点从链表的头节点开始走,最终新节点和慢指针一定会在环的入口相遇
ListNode low = head,fast = head;
while (fast!=null && fast.next != null){
low = low.next;
fast = fast.next.next;
if (low == fast){
ListNode third = head;
while (third!=low){
third = third.next;
low = low.next;
}
//走出这个循环此时third和low重合,相遇在环的入口
return third;
}
}
return null;
}
总结
在单链表的常见问题中,主要采用以下三种思想:
1、虚拟头节点法: 牵扯到链表的插入和删除时;
2、双指针、三指针: 牵扯到元素之间的比较问题。
3、快慢指针: 牵扯到寻找链表中某个特殊节点的问题;
4、递归: 牵扯到问题能拆分,则这个链表问题一定可以用递归来简化操作。
明天也需要继续加油!!!