骚戴独家笔试---算法篇
创始人
2024-02-01 22:27:03

链表

反转链表

/*
public class ListNode {
int val;
ListNode next = null;ListNode(int val) {
this.val = val;
}
}*/
import java.util.Stack;
public class Solution {public ListNode ReverseList(ListNode head) {Stack stack = new Stack<>();//把链表节点全部摘掉放到栈中while (head != null) {stack.push(head);head = head.next;}//判断链表不为空if (stack.isEmpty())return null;//获取栈顶的第一个节点ListNode node = stack.pop();//用于返回的节点,先通过下面的while循环构造链表再返回节点ListNode dummy = node;//栈中的结点全部出栈,然后重新连成一个新的链表while (!stack.isEmpty()) {//取出栈顶元素(注意这是原本栈的第二个元素开始取,前面已经取了一个)ListNode tempNode = stack.pop();//把当前节点指针的指向下一个元素node.next = tempNode;//把当前节点变成下一个节点,继续遍历node = node.next;}//最后一个结点就是反转前的头结点,一定要让他的next//等于空,否则会构成环node.next = null;return dummy;}
}

*链表内指定区间反转

import java.util.*;
/**
* Definition for singly-linked list.
* public class ListNode {
*     int val;
*     ListNode next;
*     ListNode(int x) { val = x; }
* }
*/
class Solution {public ListNode reverseBetween(ListNode head, int m, int n) {ListNode dummy = new ListNode(-1);  // 哑巴节点,指向链表的头部dummy.next = head;ListNode pre = dummy;  // pre 指向要翻转子链表的前驱节点for (int i = 1; i < m; ++i) {pre = pre.next;}head = pre.next;  // head指向翻转子链表的首部ListNode next;for (int i = m; i < n; ++i) {next = head.next;// head节点连接next节点之后链表部分,也就是向后移动一位head.next = next.next;// next节点移动到需要反转链表部分的首部next.next = pre.next;// pre继续为需要反转头节点的前驱节点pre.next = next;}return dummy.next;}
}

合并两个排序的链表

/*
public class ListNode {int val;ListNode next = null;ListNode(int val) {this.val = val;}
}*/
public class Solution {public ListNode Merge(ListNode list1,ListNode list2) {//如果有一个链表为空,返回另一个链表(特殊情况)if(list1 == null || list2 == null){return list1 != null ? list1 : list2;}/** 两个链表元素依次对比(其实只需要改变链表元素指针的指向就可以了)链表1的第一个元素小于链表2的第一个元素就把链表1的第一个元素放在第一个然后递归调用Merge方法来比较链表1的第二个元素和链表2的第一个元素,以此类推反之也一样,只是两个链表的顺序倒过来 **/if(list1.val <= list2.val){// 递归计算 list1.next, list2list1.next = Merge(list1.next, list2);return list1;}else{// 递归计算 list1, list2.nextlist2.next = Merge(list1, list2.next);return list2;} }
}

判断链表中是否有环

知识点:双指针

双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。

思路

我们都知道链表不像二叉树,每个节点只有一个val值和一个next指针,也就是说一个节点只能有一个指针指向下一个节点,不能有两个指针,那这时我们就可以说一个性质:环形链表的环一定在末尾,末尾没有NULL了。为什么这样说呢?仔细看上图,在环2,0,-4中,没有任何一个节点可以指针指出环,它们只能在环内不断循环,因此环后面不可能还有一条尾巴。如果是普通线形链表末尾一定有NULL,那我们可以根据链表中是否有NULL判断是不是有环。

但是,环形链表遍历过程中会不断循环,线形链表遍历到NULL结束了,但是环形链表何时能结束呢?我们可以用双指针技巧,同向访问的双指针,速度是快慢的,只要有环,二者就会在环内不断循环,且因为有速度差异,二者一定会相遇。

具体做法

step 1:设置快慢两个指针,初始都指向链表头。

step 2:遍历链表,快指针每次走两步,慢指针每次走一步。

step 3:如果快指针到了链表末尾,说明没有环,因为它每次走两步,所以要验证连续两步是否为NULL。

step 4:如果链表有环,那快慢双指针会在环内循环,因为快指针每次走两步,因此快指针会在环内追到慢指针,二者相遇就代表有环。

/*public class ListNode {int val;ListNode next = null;ListNode(int val) {this.val = val;}
}
*/
public class Solution {public boolean hasCycle(ListNode head) {//先判断链表为空的情况if (head == null)return false;//快慢双指针ListNode fast = head;ListNode slow = head;//快指针还没到达末尾的时候while (fast != null && fast.next != null) {//快指针移动两步fast = fast.next.next;//慢指针移动一步slow = slow.next;//相遇则有环if (fast == slow)return true;}//到末尾则没有环return false;}
}

链表中环的入口结点

快慢指针方法
通过定义slow和fast指针,slow每走一步,fast走两步,若是有环,则一定会在环的某个结点处相遇(slow == fast),根据下图分析计算,可知从相遇处到入口结点的距离与头结点与入口结点的距离相同。

具体做法

step 1:判断链表中是否有环中的方法判断链表是否有环,并找到相遇的节点。

step 2:慢指针继续在相遇节点,快指针回到链表头,两个指针以相同的速度逐个元素逐个元素开始遍历链表。

step 3:再次相遇的地方就是环的入口。

/*public class ListNode {int val;ListNode next = null;ListNode(int val) {this.val = val;}
}
*/
public class Solution {public ListNode EntryNodeOfLoop(ListNode pHead) {if (pHead == null) return null;// 定义快慢指针ListNode slow = pHead;ListNode fast = pHead;while (fast != null && fast.next != null) {// 快指针是满指针的两倍速度fast = fast.next.next;slow = slow.next;// 记录快慢指针第一次相遇的结点if (slow == fast) break;}// 若是快指针指向null,则不存在环if (fast == null || fast.next == null) return null;// 重新指向链表头部fast = pHead;// 与第一次相遇的结点相同速度出发,相遇结点为入口结点while (fast != slow) {fast = fast.next;slow = slow.next;}return fast;}
}

链表中倒数最后k个结点

思路

我们无法逆序遍历链表,就很难得到链表的倒数第kkk个元素,那我们可以试试反过来考虑,如果当前我们处于倒数第kkk的位置上,即距离链表尾的距离是kkk,那我们假设双指针指向这两个位置,二者同步向前移动,当前面个指针到了链表头的时候,两个指针之间的距离还是kkk。虽然我们没有办法让指针逆向移动,但是我们刚刚这个思路却可以正向实施。

具体做法

step 1:准备一个快指针,从链表头开始,在链表上先走k步。

step 2:准备慢指针指向原始链表头,代表当前元素,则慢指针与快指针之间的距离一直都是k。

step 3:快慢指针同步移动,当快指针到达链表尾部的时候,慢指针正好到了倒数k个元素的位置。

import java.util.*;/** public class ListNode {*   int val;*   ListNode next = null;*   public ListNode(int val) {*     this.val = val;*   }* }*/public class Solution {public ListNode FindKthToTail (ListNode pHead, int k) {int n = 0;ListNode fast = pHead;ListNode slow = pHead;//快指针先行k步for (int i = 0; i < k; i++) {if (fast != null)fast = fast.next;//达不到k步说明链表过短,没有倒数kelsereturn slow = null;}//快慢指针同步,快指针先到底,慢指针指向倒数第k个while (fast != null) {fast = fast.next;slow = slow.next;}//上面的while循环就是找到倒数的第k个节点,也就是慢指针指向的节点,所以返回慢指针就好了//(注意这里可能是一个很短的链表,因为慢节点有next指针指向后面的节点)return slow;}
}

删除链表的倒数第n个节点

具体做法

step 1:给链表添加一个表头,处理删掉第一个元素时比较方便。

step 2:准备一个快指针,在链表上先走n步。

step 3:准备慢指针指向原始链表头,代表当前元素,前序节点指向添加的表头,这样两个指针之间相距就是一直都是n。

step 4:快慢指针同步移动,当快指针到达链表尾部的时候,慢指针正好到了倒数n个元素的位置。

step 5:最后将该节点前序节点的指针指向该节点后一个节点,删掉这个节点。

import java.util.*;/** public class ListNode {*   int val;*   ListNode next = null;* }*/public class Solution {public ListNode removeNthFromEnd (ListNode head, int n) {//添加表头是为了方便处理第一个节点给删除了的情况ListNode res = new ListNode(-1);res.next = head;//当前节点/慢节点ListNode cur = head;//前序节点ListNode pre = res;ListNode fast = head;//快指针先行n步while (n != 0) {fast = fast.next;n--;}//快慢指针同步,快指针到达末尾,慢指针就到了倒数第n个位置while (fast != null) {fast = fast.next;pre = cur;cur = cur.next;}//删除该位置的节点pre.next = cur.next;//返回链表的头指针(也就是头指针指向的第一个节点)return res.next;}
}

两个链表的第一个公共结点

解题思路

使用两个指针N1,N2,一个从链表1的头节点开始遍历,我们记为N1,一个从链表2的头节点开始遍历,我们记为N2。

让N1和N2一起遍历,当N1先走完链表1的尽头(为null)的时候,则从链表2的头节点继续遍历,同样,如果N2先走完了链表2的尽头,则从链表1的头节点继续遍历,也就是说,N1和N2都会遍历链表1和链表2。

因为两个指针,同样的速度,走完同样长度(链表1+链表2),不管两条链表有无相同节点,都能够到达同时到达终点。

(N1最后肯定能到达链表2的终点,N2肯定能到达链表1的终点)。

如何得到公共节点:

  • 有公共节点的时候,N1和N2必会相遇,因为长度一样嘛,速度也一定,必会走到相同的地方的,所以当两者相等的时候,则会第一个公共的节点
  • 无公共节点的时候,此时N1和N2则都会走到终点,那么他们此时都是null,所以也算是相等了。

/*
public class ListNode {int val;ListNode next = null;ListNode(int val) {this.val = val;}
}*/
public class Solution {public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {ListNode l1 = pHead1, l2 = pHead2;while (l1 != l2) {l1 = (l1 == null) ? pHead2 : l1.next;l2 = (l2 == null) ? pHead1 : l2.next;}return l1;}
}

*链表相加

例如:链表 1 为 9->3->7,链表 2 为 6->3,最后生成新的结果链表为 1->0->0->0。
从这个例子我们可以得到:
1、两个链表的长度可能不等,需要对齐
2、相加后可能需要进位
对齐进位
因为我们无法保证两个链表长度一致,所以我们干脆从后往前对齐,跟我们整数再做加法一样

所以我们的入手则是对链表进行对齐,我们可以看到上面的图片,我们都是从后面开始对齐与计算的,所以很容易想到反转链表后进行相加。

import java.util.*;/** public class ListNode {*   int val;*   ListNode next = null;* }*/public class Solution {/**** @param head1 ListNode类* @param head2 ListNode类* @return ListNode类*/public ListNode addInList (ListNode head1, ListNode head2) {// 进行判空处理if (head1 == null)return head2;if (head2 == null) {return head1;}// 反转h1链表head1 = reverse(head1);// 反转h2链表head2 = reverse(head2);// 创建新的链表头节点ListNode head = new ListNode(-1);ListNode nHead = head;// 记录进位的数值int tmp = 0;while (head1 != null || head2 != null) {// val用来累加此时的数值(加数+加数+上一位的进位=当前总的数值)int val = tmp;// 当节点不为空的时候,则需要加上当前节点的值if (head1 != null) {val += head1.val;head1 = head1.next;}// 当节点不为空的时候,则需要加上当前节点的值if (head2 != null) {val += head2.val;head2 = head2.next;}// 求出进位tmp = val / 10;// 进位后剩下的数值即为当前节点的数值nHead.next = new ListNode(val % 10);// 下一个节点nHead = nHead.next;}// 最后当两条链表都加完的时候,进位不为0的时候,则需要再加上这个进位if (tmp > 0) {nHead.next = new ListNode(tmp);}// 重新反转回来返回return reverse(head.next);}// 反转链表ListNode reverse(ListNode head) {if (head == null)return head;ListNode cur = head;ListNode node = null;while (cur != null) {ListNode tail = cur.next;cur.next = node;node = cur;cur = tail;}return node;}
}

单链表的排序

具体做法

step 1:遍历链表,将节点值加入数组。

step 2:使用内置的排序函数对数组进行排序。

step 3:依次遍历数组和链表,按照位置将链表中的节点值修改为排序后的数组值。

import java.util.*;/** public class ListNode {*   int val;*   ListNode next = null;* }*/public class Solution {public ListNode sortInList (ListNode head) {ArrayList nums = new ArrayList<>();ListNode p = head;//遍历链表,将节点值加入数组while (p != null) {nums.add(p.val);p = p.next;}//上面的while循环以后p指向链表最后一个元素,所以这里要把p重新指向头结点,方便后面的for循环p = head;//对数组元素排序Collections.sort(nums);//遍历数组for (int i = 0; i < nums.size(); i++) {//将数组元素依次加入链表p.val = nums.get(i);p = p.next;}return head;}
}

判断一个链表是否为回文结构

思路

即然回文结构正序遍历和逆序遍历结果都是一样的,我们是不是可以尝试将正序遍历的结果与逆序遍历的结果一一比较,如果都是对应的,那很巧了!它就是回文结构!

这道题看起来解决得如此之快,但是别高兴太早,链表可没有办法逆序遍历啊。链表由前一个节点的指针指向后一个节点,指针是单向的,只能从前面到后面,我们不能任意访问,也不能从后往前。但是,另一个容器数组,可以任意访问,我们把链表中的元素值取出来放入数组中,然后判断数组是不是回文结构,这不是一样的吗?

具体做法

step 1:遍历一次链表,将元素取出放入辅助数组中。

step 2:准备另一个辅助数组,录入第一个数组的全部元素,再将其反转。

step 3:依次遍历原数组与反转后的数组,若是元素都相等则是回文结构,只要遇到一个不同的就不是回文结构。

import java.util.*;
/** public class ListNode {*   int val;*   ListNode next = null;* }*/public class Solution {public boolean isPail (ListNode head) {ArrayList nums = new ArrayList();//将链表元素取出一次放入数组while(head != null){ nums.add(head.val);head = head.next;}ArrayList temp = new ArrayList();temp = (ArrayList) nums.clone();//准备一个数组承接翻转之后的数组Collections.reverse(temp); for(int i = 0; i < nums.size(); i++){int x = nums.get(i);int y = temp.get(i);//正向遍历与反向遍历相同if(x != y) return false;}return true;}
}

这了注意上面不能像上面这样简写,有部分数据会出错

链表的奇偶重排

思路

如下图所示,第一个节点是奇数位,第二个节点是偶数,第二个节点后又是奇数位,因此可以断掉节点1和节点2之间的连接,指向节点2的后面即节点3,如红色箭头。如果此时我们将第一个节点指向第三个节点,就可以得到那么第三个节点后为偶数节点,因此我们又可以断掉节点2到节点3之间的连接,指向节点3后一个节点即节点4,如蓝色箭头。那么我们再将第二个节点指向第四个节点,又回到刚刚到情况了。

//odd连接even的后一个,即奇数位
odd.next = even.next; 
//odd进入后一个奇数位
odd = odd.next; 
//even连接后一个奇数的后一位,即偶数位
even.next = odd.next; 
//even进入后一个偶数位
even = even.next; 

具体做法

step 1:判断空链表的情况,如果链表为空,不用重排。

step 2:使用双指针odd和even分别遍历奇数节点和偶数节点,并给偶数节点链表一个头。

step 3:上述过程,每次遍历两个节点,且even在后面,因此每轮循环用even检查后两个元素是否为NULL,如果不为再进入循环进行上述连接过程。

step 4:将偶数节点头接在奇数最后一个节点后,再返回头部。

import java.util.*;/** public class ListNode {*   int val;*   ListNode next = null;*   public ListNode(int val) {*     this.val = val;*   }* }*/public class Solution {public ListNode oddEvenList (ListNode head) {//如果链表为空,不用重排if(head == null) return head;//even开头指向第二个节点,可能为空ListNode even = head.next; //odd开头指向第一个节点ListNode odd = head; //指向even开头ListNode evenhead = even; while(even != null && even.next != null){//odd连接even的后一个,即奇数位odd.next = even.next; //odd进入后一个奇数位odd = odd.next; //even连接后一个奇数的后一位,即偶数位even.next = odd.next; //even进入后一个偶数位even = even.next; } //even整体接在odd后面odd.next = evenhead; return head;}
}

删除有序链表中重复的元素-I

import java.util.*;/** public class ListNode {*   int val;*   ListNode next = null;* }*/import java.util.*;
public class Solution {public ListNode deleteDuplicates (ListNode head) {//空链表if(head == null) return null;//遍历指针ListNode cur = head; //指针当前和下一位不为空while(cur != null && cur.next != null){ //如果当前与下一位相等则忽略下一位if(cur.val == cur.next.val) cur.next = cur.next.next;//否则指针正常遍历else cur = cur.next;}return head;}
}

*删除有序链表中重复的元素-II

import java.util.*;/** public class ListNode {*   int val;*   ListNode next = null;* }*/public class Solution {public ListNode deleteDuplicates (ListNode head) {//空链表if (head == null)return null;ListNode res = new ListNode(0);//在链表前加一个表头res.next = head;ListNode cur = res;while (cur.next != null && cur.next.next != null) {//遇到相邻两个节点值相同if (cur.next.val == cur.next.next.val) {int temp = cur.next.val;//将所有相同的都跳过while (cur.next != null && cur.next.val == temp)cur.next = cur.next.next;} elsecur = cur.next;}//返回时去掉表头return res.next;}
}

二分查找/排序

二分查找

具体做法

step 1:从数组首尾开始,每次取中点值。

step 2:如果中间值等于目标即找到了,可返回下标,如果中点值大于目标,说明中点以后的都大于目标,因此目标在中点左半区间,如果中点值小于目标,则相反。

step 3:根据比较进入对应的区间,直到区间左右端相遇,意味着没有找到。

import java.util.*;
public class Solution {public int search (int[] nums, int target) {int l = 0;int r = nums.length - 1;//从数组首尾开始,直到二者相遇while (l <= r) {//每次检查中点的值int m = (l + r) / 2;if (nums[m] == target)return m;//进入左的区间if (nums[m] > target)r = m - 1;//进入右区间elsel = m + 1;}//未找到return -1;}
}

二维数组中的查找

首先看四个角,左上与右下必定为最小值与最大值,而左下与右上就有规律了:左下元素大于它上方的元素,小于它右方的元素,右上元素与之相反。既然左下角元素有这么一种规律,相当于将要查找的部分分成了一个大区间和小区间,每次与左下角元素比较,我们就知道目标值应该在哪部分中,于是可以利用分治思维来做。

具体做法

step 1:首先获取矩阵的两个边长,判断特殊情况。

step 2:首先以左下角为起点,若是它小于目标元素,则往右移动去找大的,若是他大于目标元素,则往上移动去找小的。

step 3:若是移动到了矩阵边界也没找到,说明矩阵中不存在目标值。

public class Solution {public boolean Find(int target, int [][] array) {//优先判断特殊(一行都没有)if (array.length == 0)return false;//获取二位数组的行数int n = array.length;//优先判断特殊(一列都没有)if (array[0].length == 0)return false;//获取二位数组的列数int m = array[0].length;//从最左下角的元素开始往左或往上for (int i = n - 1, j = 0; i >= 0 && j < m; ) {//元素较大,往上走if (array[i][j] > target)i--;//元素较小,往右走else if (array[i][j] < target)j++;//元素等于目标值elsereturn true;}return false;}
}

注意for循环的i >= 0 && j < m;这个条件

*寻找峰值

具体做法

step 1:二分查找首先从数组首尾开始,每次取中间值,直到首尾相遇。

step 2:如果中间值的元素大于它右边的元素,说明往右是向下,我们不一定会遇到波峰,但是那就往左收缩区间。

step 3:如果中间值大于右边的元素,说明此时往右是向上,向上一定能有波峰,那我们往右收缩区间。

step 4:最后区间收尾相遇的点一定就是波峰。

import java.util.*;
public class Solution {public int findPeakElement (int[] nums) {int left = 0;int right = nums.length - 1;//二分法while(left < right){ int mid = (left + right) / 2;//右边是往下,不一定有坡峰if(nums[mid] > nums[mid + 1])right = mid;//右边是往上,一定能找到波峰elseleft = mid + 1;}//其中一个波峰return right; }
}

*数组中的逆序对

具体做法

step 1: 划分阶段:将待划分区间从中点划分成两部分,两部分进入递归继续划分,直到子数组长度为1.

step 2: 排序阶段:使用归并排序递归地处理子序列,同时统计逆序对,因为在归并排序中,我们会依次比较相邻两组子数组各个元素的大小,并累计遇到的逆序情况。而对排好序的两组,右边大于左边时,它大于了左边的所有子序列,基于这个性质我们可以不用每次加1来统计,减少运算次数。

step 3: 合并阶段:将排好序的子序列合并,同时累加逆序对。

public class Solution {public int mod = 1000000007;public int mergeSort(int left, int right, int [] data, int [] temp){//停止划分if(left >= right)    return 0;//取中间int mid = (left + right) / 2; //左右划分合并int res = mergeSort(left, mid, data, temp) + mergeSort(mid + 1, right, data, temp); //防止溢出res %= mod;  int i = left, j = mid + 1;for(int k = left; k <= right; k++)temp[k] = data[k];for(int k = left; k <= right; k++){if(i == mid + 1)data[k] = temp[j++];else if(j == right + 1 || temp[i] <= temp[j])data[k] = temp[i++];//左边比右边大,答案增加else{ data[k] = temp[j++];// 统计逆序对res += mid - i + 1; }}return res % mod;}public int InversePairs(int [] array) {int n = array.length;int[] res = new int[n];return mergeSort(0, n - 1, array, res);}
}

旋转数组的最小数字

思路

旋转数组将原本有序的数组分成了两部分有序的数组,因为在原始有序数组中,最小的元素一定是在首位,旋转后无序的点就是最小的数字。我们可以将旋转前的前半段命名为A,旋转后的前半段命名为B,旋转数组即将AB变成了BA,我们想知道最小的元素到底在哪里。

因为A部分和B部分都是各自有序的,所以我们还是想用分治来试试,每次比较中间值,确认目标值(最小元素)所在的区间。

具体做法

step 1:双指针指向旋转后数组的首尾,作为区间端点。

step 2:若是区间中点值大于区间右界值,则最小的数字一定在中点右边。

step 3:若是区间中点值等于区间右界值,则是不容易分辨最小数字在哪半个区间,比如[1,1,1,0,1],应该逐个缩减右界。

step 4:若是区间中点值小于区间右界值,则最小的数字一定在中点左边。

step 5:通过调整区间最后即可锁定最小值所在。

import java.util.ArrayList;
public class Solution {public int minNumberInRotateArray(int [] array) {int left = 0;int right = array.length - 1;while (left < right) {int mid = (left + right) / 2;//最小的数字在mid右边if (array[mid] > array[right])left = mid + 1;//无法判断,一个一个试else if (array[mid] == array[right])right--;//最小数字要么是mid要么在mid左边elseright = mid;}return array[left];}
}

*比较版本号

具体做法

step 1:利用两个指针表示字符串的下标,分别遍历两个字符串。

step 2:每次截取点之前的数字字符组成数字,即在遇到一个点之前,直接取数字,加在前面数字乘10的后面。(因为int会溢出,这里采用long记录数字)

step 3:然后比较两个数字大小,根据大小关系返回1或者-1,如果全部比较完都无法比较出大小关系,则返回0.

import java.util.*;
public class Solution {public int compare (String version1, String version2) {int n1 = version1.length();int n2 = version2.length();int i = 0, j = 0;//直到某个字符串结束while(i < n1 || j < n2){long num1 = 0;//从下一个点前截取数字while(i < n1 && version1.charAt(i) != '.'){ num1 = num1 * 10 + (version1.charAt(i) - '0');i++;}//跳过点i++; long num2 = 0;//从下一个点前截取数字while(j < n2 && version2.charAt(j) != '.'){ num2 = num2 * 10 + (version2.charAt(j) - '0');j++;}//跳过点j++; //比较数字大小if(num1 > num2) return 1;if(num1 < num2)return -1;}//版本号相同return 0; }
}

二叉树

二叉树的前序遍历

这里的#应该只是区分根节点和其他节点的,不是数组的内容

思路

我们都知道递归,是不断调用自己,计算内部实现递归的时候,是将之前的父问题存储在栈中,先去计算子问题,等到子问题返回给父问题后再从栈中将父问题弹出,继续运算父问题。因此能够递归解决的问题,我们似乎也可以用栈来试一试。

根据前序遍历“根左右”的顺序,首先要遍历肯定是根节点,然后先遍历左子树,再遍历右子树。递归中我们是先进入左子节点作为子问题,等左子树结束,再进入右子节点作为子问题。

这里我们同样可以这样做,它无非相当于把父问题挂进了栈中,等子问题结束再从栈中弹出父问题,从父问题进入右子树,我们这里已经访问过了父问题,不妨直接将右子节点挂入栈中,然后下一轮先访问左子节点。要怎么优先访问左子节点呢?同样将它挂入栈中,依据栈的后进先出原则,下一轮循环必然它要先出来,如此循环,原先父问题的右子节点被不断推入栈深处,只有左子树全部访问完毕,才会弹出继续访问。

具体做法

step 1:优先判断树是否为空,空树不遍历。

step 2:准备辅助栈,首先记录根节点。

step 3:每次从栈中弹出一个元素,进行访问,然后验证该节点的左右子节点是否存在,存的话的加入栈中,优先加入右节点。

import java.util.*;/** public class TreeNode {*   int val = 0;*   TreeNode left = null;*   TreeNode right = null;*   public TreeNode(int val) {*     this.val = val;*   }* }*/
public class Solution {public int[] preorderTraversal (TreeNode root) {//添加遍历结果的数组List list = new ArrayList(); Stack s = new Stack();//空树返回空数组if(root == null) return new int[0];//根节点先进栈s.push(root); while(!s.isEmpty()){//每次栈顶就是访问的元素TreeNode node = s.pop(); list.add(node.val);//如果右边还有右子节点进栈if(node.right != null) s.push(node.right);//如果左边还有左子节点进栈if(node.left != null) s.push(node.left);}//返回的结果int[] res = new int[list.size()]; for(int i = 0; i < list.size(); i++)res[i] = list.get(i);return res;}
}

二叉树的中序遍历

具体做法

step 1:优先判断树是否为空,空树不遍历。

step 2:准备辅助栈,当二叉树节点为空了且栈中没有节点了,我们就停止访问。

step 3:从根节点开始,每次优先进入每棵的子树的最左边一个节点,我们将其不断加入栈中,用来保存父问题。

step 4:到达最左后,可以开始访问,如果它还有右节点,则将右边也加入栈中,之后右子树的访问也是优先到最左。


/** public class TreeNode {*   int val = 0;*   TreeNode left = null;*   TreeNode right = null;*   public TreeNode(int val) {*     this.val = val;*   }* }*/import java.util.*;
public class Solution {public int[] inorderTraversal (TreeNode root) {//添加遍历结果的数组List list = new ArrayList();Stack s = new Stack();//空树返回空数组if (root == null)return new int[0];//当树节点不为空或栈中有节点时while (root != null || !s.isEmpty()) {//每次找到最左节点while (root != null) {s.push(root);root = root.left;}//访问该节点TreeNode node = s.pop();list.add(node.val);//进入右节点root = node.right;}//返回的结果int[] res = new int[list.size()];for (int i = 0; i < list.size(); i++)res[i] = list.get(i);return res;}
}

*二叉树的后序遍历

具体做法

step 1:开辟一个辅助栈,用于记录要访问的子节点,开辟一个前序指针pre。

step 2:从根节点开始,每次优先进入每棵的子树的最左边一个节点,我们将其不断加入栈中,用来保存父问题。

step 3:弹出一个栈元素,看成该子树的根,判断这个根的右边有没有节点或是有没有被访问过,如果没有右节点或是被访问过了,可以访问这个根,并将前序节点标记为这个根。

step 4:如果没有被访问,那这个根必须入栈,进入右子树继续访问,只有右子树结束了回到这里才能继续访问根。

import java.util.*;/** public class TreeNode {*   int val = 0;*   TreeNode left = null;*   TreeNode right = null;*   public TreeNode(int val) {*     this.val = val;*   }* }*/import java.util.*;
public class Solution {public int[] postorderTraversal (TreeNode root) {//添加遍历结果的数组List list = new ArrayList();Stack s = new Stack();TreeNode pre = null;while (root != null || !s.isEmpty()) {//每次先找到最左边的节点while (root != null) {s.push(root);root = root.left;}//弹出栈顶TreeNode node = s.pop();//如果该元素的右边没有或是已经访问过if (node.right == null || node.right == pre) {//访问中间的节点list.add(node.val);//且记录为访问过了pre = node;} else {//该节点入栈s.push(node);//先访问右边root = node.right;}}//返回的结果int[] res = new int[list.size()];for (int i = 0; i < list.size(); i++)res[i] = list.get(i);return res;}
}

堆/栈/队列

用两个栈实现队列

思路:

元素进栈以后,只能优先弹出末尾元素,但是队列每次弹出的却是最先进去的元素,如果能够将栈中元素全部取出来,才能访问到最前面的元素,此时,可以用另一个栈来辅助取出。

import java.util.Stack;
public class Solution {Stack stack1 = new Stack();Stack stack2 = new Stack();public void push(int node) {stack1.push(node);}public int pop() {//将第一个栈中内容弹出放入第二个栈中while (!stack1.isEmpty())stack2.push(stack1.pop());//第二个栈栈顶就是最先进来的元素,即队首int res = stack2.pop();//再将第二个栈的元素放回第一个栈while (!stack2.isEmpty())stack1.push(stack2.pop());return res;}
}

包含min函数的栈

import java.util.Stack;public class Solution {//用于栈的push 与 popStack s1 = new Stack();//用于存储最小minStack s2 = new Stack();public void push(int node) {s1.push(node);//空或者新元素较小,则入栈if (s2.isEmpty() || s2.peek() > node)s2.push(node);else//重复加入栈顶s2.push(s2.peek());}//这里注意要同时pop两个栈来保证元素同步,因为第二个栈是有重复的public void pop() {s1.pop();s2.pop();}public int top() {return s1.peek();}public int min() {return s2.peek();}
}

有效括号序列

思路

括号的匹配规则应该符合先进后出原理:最外层的括号即最早出现的左括号,也对应最晚出现的右括号,即先进后出,因此可以使用同样先进后出的栈:遇到左括号就将相应匹配的右括号加入栈中,后续如果是合法的,右括号来的顺序就是栈中弹出的顺序。

具体做法

step 1:创建辅助栈,遍历字符串。

step 2:每次遇到小括号的左括号、中括号的左括号、大括号的左括号,就将其对应的呦括号加入栈中,期待在后续遇到。

step 3:如果没有遇到左括号但是栈为空,说明直接遇到了右括号,不合法。

step 4:其他情况下,如果遇到右括号,刚好会与栈顶元素相同,弹出栈顶元素继续遍历。

step 5:理论上,只要括号是匹配的,栈中元素最后是为空的,因此检查栈是否为空即可最后判断是否合法。

import java.util.*;
public class Solution {public boolean isValid (String s) {//辅助栈Stack st = new Stack();//遍历字符串for (int i = 0; i < s.length(); i++) {//遇到左小括号if (s.charAt(i) == '(')//期待遇到右小括号st.push(')');//遇到左中括号else if (s.charAt(i) == '[')//期待遇到右中括号st.push(']');//遇到左打括号else if (s.charAt(i) == '{')//期待遇到右打括号st.push('}');//st.isEmpty() 如果没有遇到左括号但是栈为空,说明直接遇到了右括号//st.pop() != s.charAt(i)如果遇到右括号,刚好会与栈顶元素相同else if (st.isEmpty() || st.pop() != s.charAt(i))return false;}//栈中是否还有元素,没有元素说明都匹配成功了,有元素说明还有的没有匹配成功return st.isEmpty();}
}

最小的K个数

具体做法

step 1:利用input数组中前k个元素,构建一个大小为k的大顶堆,堆顶为这k个元素的最大值。

step 2:对于后续的元素,依次比较其与堆顶的大小,若是比堆顶小,则堆顶弹出,再将新数加入堆中,直至数组结束,保证堆中的k个最小。

step 3:最后将堆顶依次弹出即是最小的k个数。

import java.util.*;
public class Solution {public ArrayList GetLeastNumbers_Solution(int [] input, int k) {ArrayList res = new ArrayList();//排除特殊情况if (k == 0 || input.length == 0)return res;//大根堆PriorityQueue q = new PriorityQueue<>((o1, o2)->o2.compareTo(o1));//构建一个k个大小的堆//(只需要加数据,它自己会构建一个大顶堆,注意这里是先把input数组的前四个元素加到堆中)for (int i = 0; i < k; i++)q.offer(input[i]);//然后把input数组的第5个元素开始一个个与第堆顶元素(也就是最大的值)比较//小的值加入堆,大的值弹出不要了,所以最后堆中就剩下来最小的四个值for (int i = k; i < input.length; i++) {//较小元素入堆if (q.peek() > input[i]) {//大的值弹出不要了(也就是堆顶元素)q.poll();//小的值加入堆q.offer(input[i]);}}//堆中元素取出入数组for (int i = 0; i < k; i++)res.add(q.poll());return res;}
}

寻找第K大

具体做法

step 1:进行一次快排,大元素在左,小元素在右,得到的标杆j点.在此之前要使用随机数获取标杆元素,防止数据分布导致每次划分不能均衡。

step 2:如果 j + 1 = k ,那么j点就是第K大。

step 3:如果 j + 1 > k,则第k大的元素在左半段,更新high = j - 1,执行step 1。

step 4:如果 j + 1 < k,则第k大的元素在右半段,更新low = j + 1, 再执行step 1.

import java.util.*;
public class Solution {//交换函数Random r = new Random();public static void swap(int arr[], int a, int b) {int temp = arr[a];arr[a] = arr[b];arr[b] = temp;}public int partition(int[] a, int low, int high, int k){//随机快排划分int x = Math.abs(r.nextInt()) % (high - low + 1) + low;swap(a, low, x);int v = a[low];int i = low + 1;int j = high;while(true){//小于标杆的在右while(j >= low + 1 && a[j] < v) j--;//大于标杆的在左while(i <= high && a[i] > v) i++;if(i > j) break;swap(a, i, j);i++;j--;}swap(a, low, j);//从0开始,所以为第j+1大if(j + 1 == k)return a[j];//继续划分else if(j + 1 < k)return partition(a, j + 1, high, k);elsereturn partition(a, low, j - 1, k);}public int findKth(int[] a, int n, int K) {return partition(a, 0, n - 1, K);}
}

import java.util.*;public class Solution {public int findKth(int[] a, int n, int K) {// 暂存K个较大的值,优先队列默认是自然排序(升序),队头元素(根)是堆内的最小元素,也就是小根堆PriorityQueue queue = new PriorityQueue<>(K);// 遍历每一个元素,调整小根堆for (int num : a) {// 对于小根堆来说,只要没满就可以加入(不需要比较);如果满了,才判断是否需要替换第一个元素if (queue.size() < K) {queue.add(num);} else {// 在小根堆内,存储着K个较大的元素,根是这K个中最小的,如果出现比根还要大的元素,说明可以替换根if (num > queue.peek()) {queue.poll(); // 高个中挑矮个,矮个淘汰queue.add(num);}}}return queue.isEmpty() ? 0 : queue.peek();}
}

数据流中的中位数

具体做法

step 1:用一数组存储输入的数据流。

step 2:Insert函数在插入的同时,遍历之前存储在数组中的数据,按照递增顺序依次插入,如此一来,加入的数据流便是有序的。

step 3:GetMedian函数可以根据下标直接访问中位数,分为数组为奇数个元素和偶数个元素两种情况。记得需要类型转换为double。

import java.util.*;
public class Solution {private ArrayList val = new ArrayList();public void Insert(Integer num) {if (val.isEmpty())//val中没有数据,直接加入val.add(num);//val中有数据,需要插入排序else {int i = 0;//遍历找到插入点for (; i < val.size(); i++) {if (num <= val.get(i))break;}//插入相应位置val.add(i, num);}}public Double GetMedian() {int n = val.size();//奇数个数字if (n % 2 == 1)//类型转换return (double)val.get(n / 2);//偶数个数字else {double a = val.get(n / 2);double b = val.get(n / 2 - 1);return (a + b) / 2;}}
}

表达式求值

思路

对于上述两个要求,我们要考虑的是两点,一是处理运算优先级的问题,二是处理括号的问题。

  • 处理优先级问题,那必定是乘号有着优先运算的权利,加号减号先一边看,我们甚至可以把减号看成加一个数的相反数,则这里只有乘法和加法,那我们优先处理乘法,遇到乘法,把前一个数和后一个数乘起来,遇到加法就把这些数字都暂时存起来,最后乘法处理完了,就剩余加法,把之前存起来的数字都相加就好了。
  • 处理括号的问题,我们可以将括号中的部分看成一个新的表达式,即一个子问题,因此可以将新的表达式递归地求解,得到一个数字,再运算:
    • 终止条件: 每次遇到左括号意味着进入括号子问题进行计算,那么遇到右括号代表这个递归结束。
    • 返回值: 将括号内部的计算结果值返回。
    • 本级任务: 遍历括号里面的字符,进行计算。

具体做法

step 1:使用栈辅助处理优先级,默认符号为加号。

step 2:遍历字符串,遇到数字,则将连续的数字字符部分转化为int型数字。

step 3:遇到左括号,则将括号后的部分送入递归,处理子问题;遇到右括号代表已经到了这个子问题的结尾,结束继续遍历字符串,将子问题的加法部分相加为一个数字,返回。

step 4:当遇到符号的时候如果是+,得到的数字正常入栈,如果是-,则将其相反数入栈,如果是*,则将栈中内容弹出与后一个元素相乘再入栈。

step 5:最后将栈中剩余的所有元素,进行一次全部相加。

import java.util.*;
public class Solution {public ArrayList function(String s, int index) {Stack stack = new Stack();int num = 0;char op = '+';int i;for (i = index; i < s.length(); i++) {//数字转换成int数字//判断是否为数字if (s.charAt(i) >= '0' && s.charAt(i) <= '9') {num = num * 10 + s.charAt(i) - '0';if (i != s.length() - 1)continue;}//碰到'('时,把整个括号内的当成一个数字处理if (s.charAt(i) == '(') {//递归处理括号ArrayList res = function(s, i + 1);num = res.get(0);i = res.get(1);if (i != s.length() - 1)continue;}switch (op) {//加减号先入栈case '+':stack.push(num);break;case '-'://相反数stack.push(-num);break;//优先计算乘号case '*':int temp = stack.pop();stack.push(temp * num);break;}num = 0;//右括号结束递归if (s.charAt(i) == ')')break;elseop = s.charAt(i);}int sum = 0;//栈中元素相加while (!stack.isEmpty())sum += stack.pop();ArrayList temp = new ArrayList();temp.add(sum);temp.add(i);return temp;}public int solve (String s) {ArrayList res = function(s, 0);return res.get(0);}
}

哈希

两数之和

知识点:哈希表

哈希表是一种根据关键码(key)直接访问值(value)的一种数据结构。而这种直接访问意味着只要知道key就能在O(1)时间内得到value,因此哈希表常用来统计频率、快速检验某个元素是否出现过等。

思路

我们能想到最直观的解法,可能就是两层遍历,将数组所有的二元组合枚举一遍,看看是否是和为目标值,但是这样太费时间了,既然加法这么复杂,我们是不是可以尝试一下减法:对于数组中出现的一个数a,如果目标值减去a的值已经出现过了,那这不就是我们要找的一对元组吗?这种时候,快速找到已经出现过的某个值,可以考虑使用哈希表快速检验某个元素是否出现过这一功能。

具体做法

step 1:构建一个哈希表,其中key值为遍历数组过程中出现过的值,value值为其相应的下标,因为我们最终要返回的是下标。

step 2:遍历数组每个元素,如果目标值减去该元素的结果在哈希表中存在,说明我们先前遍历的时候它出现过,根据记录的下标,就可以得到结果。

step 3:如果相减后的结果没有在哈希表中,说明先前遍历的元素中没有它对应的另一个值,那我们将它加入哈希表,等待后续它匹配的那个值出现即可。

step 4:需要注意最后的结果是下标值加1.

import java.util.*;
public class Solution {public int[] twoSum (int[] numbers, int target) {int[] res = new int[0];//创建哈希表,两元组分别表示值、下标HashMap hash = new HashMap();//在哈希表中查找target-numbers[i]for (int i = 0; i < numbers.length; i++) {int temp = target - numbers[i];//若是没找到,将此信息计入哈希表if (!hash.containsKey(temp))hash.put(numbers[i], i);//否则返回两个下标+1elsereturn new int[] {hash.get(temp) + 1, i + 1};}return res;}
}

数组中出现次数超过一半的数字

思路

首先我们分析一下,数组某个元素出现次数超过了数组长度的一半,那它肯定出现最多,而且只要超过了一半,其他数字不可能超过一半了,必定是它。

如果给定的数组是有序的,那我们在连续的相同数字中找到出现次数最多即可,但是题目没有要求有序,一种方法是对数组排序后解决,但是时间复杂度就上去了。那我们可以考虑遍历一次数组统计各个元素出现的次数,找到出现次数大于数组长度一半的那个数字。

具体做法

step 1:构建一个哈希表,统计数组元素各自出现了多少次,即key值为数组元素,value值为其出现次数。

step 2:遍历数组,每遇到一个元素就把哈希表中相应key值的value值加1,用来统计出现次数。

step 3:本来可以统计完了之后统一遍历哈希表找到频次大于数组长度一半的key值,但是根据我们上面加粗的点,只要它出现超过了一半,不管后面还有没有,必定就是这个元素了,因此每次统计后,我们都可以检查value值是否大于数组长度的一半,如果大于则找到了。

import java.util.*;
public class Solution {public int MoreThanHalfNum_Solution(int [] array) {//哈希表统计每个数字出现的次数HashMap mp = new HashMap();//遍历数组for (int i = 0; i < array.length; i++) {//统计频率if (!mp.containsKey(array[i]))mp.put(array[i], 1);elsemp.put(array[i], mp.get(array[i]) + 1);//一旦有个数大于长度一半的情况即可返回if (mp.get(array[i]) > array.length / 2)return array[i];}return 0;}
}

数组中只出现一次的两个数字

思路

既然有两个数字只出现了一次,我们就统计每个数字的出现次数,利用哈希表的快速根据key值访问其频率值。

具体做法

step 1:遍历数组,用哈希表统计每个数字出现的频率。

step 2:然后再遍历一次数组,对比哈希表,找到出现频率为1的两个数字。

step 3:最后整理次序输出。

import java.util.*;
public class Solution {public int[] FindNumsAppearOnce (int[] array) {HashMap mp = new HashMap(); ArrayList res = new ArrayList();//遍历数组for(int i = 0; i < array.length; i++) //统计每个数出现的频率if(!mp.containsKey(array[i]))mp.put(array[i], 1);elsemp.put(array[i], mp.get(array[i]) + 1);//再次遍历数组for(int i = 0; i < array.length; i++) //找到频率为1的两个数if(mp.get(array[i]) == 1) res.add(array[i]);//整理次序if(res.get(0) < res.get(1)) return new int[] {res.get(0), res.get(1)};elsereturn new int[] {res.get(1), res.get(0)};}
}

缺失的第一个正整数

具体做法

step 1:构建一个哈希表,用于记录数组中出现的数字。

step 2:从1开始,遍历到n,查询哈希表中是否有这个数字,如果没有,说明它就是数组缺失的第一个正整数,即找到。

step 3:如果遍历到最后都在哈希表中出现过了,那缺失的就是n+1.

import java.util.*;
public class Solution {public int minNumberDisappeared (int[] nums) {int n = nums.length;HashMap mp = new HashMap();//哈希表记录数组中出现的每个数字for (int i = 0; i < n; i++)mp.put(nums[i], 1);int res = 1;//从1开始找到哈希表中第一个没有出现的正整数while (mp.containsKey(res))res++;return res;}
}

*三数之和

具体做法

step 1:排除边界特殊情况。

step 2:既然三元组内部要求非降序排列,那我们先得把这个无序的数组搞有序了,使用sort函数优先对其排序。

step 3:得到有序数组后,遍历该数组,对于每个遍历到的元素假设它是三元组中最小的一个,那么另外两个一定在后面。

step 4:需要三个数相加为0,则另外两个数相加应该为上述第一个数的相反数,我们可以利用双指针在剩余的子数组中找有没有这样的数对。双指针指向剩余子数组的首尾,如果二者相加为目标值,那么可以记录,而且二者中间的数字相加可能还会有。

step 5:如果二者相加大于目标值,说明右指针太大了,那就将其左移缩小,相反如果二者相加小于目标值,说明左指针太小了,将其右移扩大,直到两指针相遇,剩余子数组找完了。

注:对于三个数字都要判断是否相邻有重复的情况,要去重。

import java.util.*;
public class Solution {public ArrayList> threeSum(int[] num) {ArrayList > res = new ArrayList>();int n = num.length;//不够三元组if(n < 3) return res;//排序Arrays.sort(num); for(int i = 0; i < n - 2; i++){if(i != 0 && num[i] == num[i - 1])continue;//后续的收尾双指针int left = i + 1; int right = n - 1;//设置当前数的负值为目标int target = -num[i]; while(left < right){//双指针指向的二值相加为目标,则可以与num[i]组成0if(num[left] + num[right] == target){ArrayList temp = new ArrayList();temp.add(num[i]);temp.add(num[left]);temp.add(num[right]);res.add(temp);while(left + 1 < right && num[left] == num[left + 1])//去重left++; while(right - 1 > left && num[right] == num[right - 1])//去重right--; //双指针向中间收缩left++; right--;}//双指针指向的二值相加大于目标,右指针向左else if(num[left] + num[right] > target)right--;//双指针指向的二值相加小于目标,左指针向右else left++;}}return res;}
}

递归/回溯

*没有重复项数字的全排列

思路

全排列就是对数组元素交换位置,使每一种排列都可能出现。因为题目要求按照字典序排列输出,那毫无疑问第一个排列就是数组的升序排列,它的字典序最小,后续每个元素与它后面的元素交换一次位置就是一种排列情况,但是如果要保持原来的位置不变,那就不应该从它后面的元素开始交换而是从自己开始交换才能保证原来的位置不变,不会漏情况。

如何保证每个元素能和从自己开始后的每个元素都交换位置,这种时候我们可以考虑递归。为什么可以使用递归?我们可以看数组[1,2,3,4],如果遍历经过一个元素2以后,那就相当于我们确定了数组到该元素为止的前半部分,前半部分1和2的位置都不用变了,只需要对3,4进行排列,这对于后半部分而言同样是一个全排列,同样要对从每个元素开始往后交换位置,因此后面部分就是一个子问题。那我们考虑递归的几个条件:

  • 终止条件: 要交换位置的下标到了数组末尾,没有可交换的了,那这就构成了一个排列情况,可以加入输出数组。
  • 返回值: 每一级的子问题应该把什么东西传递给父问题呢,这个题中我们是交换数组元素位置,前面已经确定好位置的元素就是我们返还给父问题的结果,后续递归下去会逐渐把整个数组位置都确定,形成一种排列情况。
  • 本级任务: 每一级需要做的就是遍历从它开始的后续元素,每一级就与它交换一次位置。

如果只是使用递归,我们会发现,上例中的1与3交换位置以后,如果2再与4交换位置的时候,我们只能得到3412这种排列,无法得到1432这种情况。这是因为遍历的时候1与3交换位置在2与4交换位置前面,递归过程中就默认将后者看成了前者的子问题,但是其实我们1与3没有交换位置的情况都没有结束,相当于上述图示中只进行了第一个分支。因此我们用到了回溯。处理完1与3交换位置的子问题以后,我们再将其交换回原来的情况,相当于上述图示回到了父节点,那后续完整的情况交换我们也能得到。

//遍历后续的元素
for(int i = index; i < num.size(); i++){ //交换二者swap(num, i, index); //继续往后找recursion(res, num, index + 1); //回溯swap(num, i, index); 
}

具体做法

step 1:先将数组排序,获取字典序最小的排列情况。

step 2:递归的时候根据当前下标,遍历后续的元素,交换二者位置后,进入下一层递归。

step 3:处理完一分支的递归后,将交换的情况再换回来进行回溯,进入其他分支。

step 4:当前下标到达数组末尾就是一种排列情况。

import java.util.*;
public class Solution {//交换位置函数private void swap(ArrayList num, int i1, int i2 ){int temp = num.get(i1);num.set(i1, num.get(i2));num.set(i2, temp);}public void recursion(ArrayList> res, ArrayList num,int index) {//分枝进入结尾,找到一种排列if (index == num.size() - 1) {res.add(num);} else {//遍历后续的元素for (int i = index; i < num.size(); i++) {//交换二者swap(num, i, index);//继续往后找recursion(res, num, index + 1);//回溯swap(num, i, index);}}}public ArrayList> permute(int[] num) {//先按字典序排序Arrays.sort(num);ArrayList > res = new ArrayList>();ArrayList nums = new ArrayList();//数组转ArrayListfor (int i = 0; i < num.length; i++)nums.add(num[i]);recursion(res, nums, 0);return res;}
}

字符串

字符串变形

思路

将单词位置的反转,那肯定前后都是逆序,不如我们先将整个字符串反转,这样是不是单词的位置也就随之反转了。但是单词里面的成分也反转了啊,既然如此我们再将单词里面的部分反转过来就行。

具体做法

step 1:遍历字符串,遇到小写字母,转换成大写,遇到大写字母,转换成小写,遇到空格正常不变。

step 2:第一次反转整个字符串,这样基本的单词逆序就有了,但是每个单词的字符也是逆的。

step 3:再次遍历字符串,以每个空间为界,将每个单词反转回正常。

import java.util.*;
public class Solution {public String trans(String s, int n) {if (n == 0)return s;StringBuffer res = new StringBuffer();for (int i = 0; i < n; i++) {//大小写转换if (s.charAt(i) <= 'Z' && s.charAt(i) >= 'A')res.append((char)(s.charAt(i) - 'A' + 'a'));else if (s.charAt(i) >= 'a' && s.charAt(i) <= 'z')res.append((char)(s.charAt(i) - 'a' + 'A'));else//空格直接复制res.append(s.charAt(i));}//翻转整个字符串res = res.reverse();//翻转整个字符串的每个单词for (int i = 0; i < n; i++) {int j = i;//以空格为界,找到每个单词while (j < n && res.charAt(j) != ' ')j++;//找到每个单词后反转替换res中的单词,重置i的位置,继续找后一个单词String temp = res.substring(i, j);StringBuffer buffer = new StringBuffer(temp);temp = buffer.reverse().toString();res.replace(i, j, temp);i = j;}return res.toString();}
}

最长公共前缀

思路

既然是公共前缀,那我们可以用一个字符串与其他字符串进行比较,从第一个字符开始,逐位比较,找到最长公共子串。

具体做法

step 1:处理数组为空的特殊情况。

step 2:因为最长公共前缀的长度不会超过任何一个字符串的长度,因此我们逐位就以第一个字符串为标杆,遍历第一个字符串的所有位置,取出字符。

step 3:遍历数组中后续字符串,依次比较其他字符串中相应位置是否为刚刚取出的字符,如果是,循环继续,继续查找,如果不是或者长度不足,说明从第i位开始不同,前面的都是公共前缀。

step 4:如果遍历结束都相同,最长公共前缀最多为第一个字符串。

import java.util.*;
public class Solution {public String longestCommonPrefix (String[] strs) {int n = strs.length;//空字符串数组if (n == 0)return "";//遍历第一个字符串的长度for (int i = 0; i < strs[0].length(); i++) {char temp = strs[0].charAt(i);//遍历后续的字符串for (int j = 1; j < n; j++)//比较每个字符串该位置是否和第一个相同,i == strs[j].length()是长度不足if (i == strs[j].length() || strs[j].charAt(i) != temp)//不相同则结束,也就是截取第一个字符串的指定长度return strs[0].substring(0, i);}//上面截取后第一个字符串就是最长公共前缀return strs[0];}
}

*验证IP地址

思路

我们可以先对IP字符串进行分割,然后依次判断每个分割是否符合要求。

具体做法

step 1:写一个split函数(或者内置函数)。

step 2:遍历IP字符串,遇到.或者:将其分开储存在一个数组中。

step 3:遍历数组,对于IPv4,需要依次验证分组为4,分割不能缺省,没有前缀0或者其他字符,数字在0-255范围内。

step 4:对于IPv6,需要依次验证分组为8,分割不能缺省,每组不能超过4位,不能出现除数字小大写a-f以外的字符。

import java.util.*;
public class Solution {boolean isIPv4 (String IP) {if(IP.indexOf('.') == -1){return false;}String[] s = IP.split("\\.");  //IPv4必定为4组if(s.length != 4)  return false;for(int i = 0; i < s.length; i++){//不可缺省,有一个分割为零,说明两个点相连if(s[i].length() == 0)  return false;//比较数字位数及不为零时不能有前缀零if(s[i].length() < 0 || s[i].length() > 3 || (s[i].charAt(0)=='0' && s[i].length() != 1))  return false;int num = 0;//遍历每个分割字符串,必须为数字for(int j = 0; j < s[i].length(); j++){  char c = s[i].charAt(j);if (c < '0' || c > '9') return false;//转化为数字比较,0-255之间num = num * 10 + (int)(c - '0');  if(num < 0 || num > 255) return false;}}    return true;}boolean isIPv6 (String IP) {if (IP.indexOf(':') == -1) {return false;}String[] s = IP.split(":",-1);//IPv6必定为8组if(s.length != 8){  return false;}for(int i = 0; i < s.length; i++){ //每个分割不能缺省,不能超过4位if(s[i].length() == 0 || s[i].length() > 4){ return false; }for(int j = 0; j < s[i].length(); j++){//不能出现a-fA-F以外的大小写字符char c = s[i].charAt(j);boolean expr = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') ;if(!expr){return false;}}}return true;}String solve(String IP) {if(isIPv4(IP))return "IPv4";else if(isIPv6(IP))return "IPv6";return "Neither";}
}

*大数加法

思路

大整数相加,就可以按照整数相加的方式,从个位开始,逐渐往上累加,换到字符串中就是从两个字符串的末尾开始相加。

具体做法

step 1:若是其中一个字符串为空,直接返回另一个,不用加了。

step 2:交换两个字符串的位置,我们是s为较长的字符串,t为较短的字符串,结果也记录在较长的字符串中。

step 3:从后往前遍历字符串s,每次取出字符转数字,加上进位制,将下标转换为字符串t中从后往前相应的下标,如果下标为非负数则还需要加上字符串t中相应字符转化的数字。

step 4:整型除法取进位,取模算法去掉十位,将计算后的结果放入较长数组对应位置。

step 5:如果遍历结束,进位值还有,则需要直接在字符串s前增加一个字符‘1’

import java.util.*;
public class Solution {public String solve (String s, String t) {//若是其中一个为空,返回另一个if (s.length() <= 0)return t;if (t.length() <= 0)return s;//让s为较长的,t为较短的if (s.length() < t.length()) {String temp = s;s = t;t = temp;}int carry = 0; //进位标志char[] res = new char[s.length()];//从后往前遍历较长的字符串for (int i = s.length() - 1; i >= 0; i--) {//转数字加上进位int temp = s.charAt(i) - '0' + carry;//转较短的字符串相应的从后往前的下标int j = i - s.length() + t.length();//如果较短字符串还有if (j >= 0)//转数组相加temp += t.charAt(j) - '0';//取进位carry = temp / 10;//去十位temp = temp % 10;//修改结果res[i] = (char)(temp + '0');}String output = String.valueOf(res);//最后的进位if (carry == 1)output = '1' + output;return output;}
}

双指针

合并两个有序的数组

思路

既然是两个已经排好序的数组,如果可以用新的辅助数组,那很容易我们可以借助归并排序的思想,将排好序的两个子数组合并到一起。但是这道题要求我们在数组A上面添加,那因为数组A后半部分相当于为空,则我们可以考虑逆向使用归并排序思想,从较大的开始排。对于两个数组每次选取较大的值,因此需要使用两个同时向前遍历的双指针。

具体做法

step 1:使用三个指针,i指向数组A的最大元素,j指向数组B的最大元素,k指向数组A空间的结尾处。

step 2:从两个数组最大的元素开始遍历,直到某一个结束,每次取出较大的一个值放入数组A空间的最后,然后指针一次往前。

step 3:如果数组B先遍历结束,数组A前半部分已经存在了,不用管;但是如果数组A先遍历结束,则需要把数组B剩余的前半部分依次逆序加入数组A前半部分,类似归并排序最后的步骤。

import java.util.*;
public class Solution {public void merge(int A[], int m, int B[], int n) {//指向数组A的结尾int i = m - 1; //指向数组B的结尾int j = n - 1; //指向数组A空间的结尾处int k = m + n - 1; //从两个数组最大的元素开始,直到某一个数组遍历完while(i >= 0 && j >= 0){ //将较大的元素放到最后if(A[i] > B[j]) A[k--] = A[i--];elseA[k--] = B[j--];}//数组A遍历完了,数组B还有,则还需要添加到数组A前面if(i < 0){ while(j >= 0)A[k--] = B[j--];} //数组B遍历完了,数组A前面正好有,不用再添加}
}

判断是否为回文字符串

具体做法

step 1:准备两个指针,一个在字符串首,一个在字符串尾。

step 2:在首的指针往后走,在尾的指针往前走,依次比较路过的两个字符是否相等,若是不相等则直接就不是回文。

step 3:直到两指针在中间相遇,都还一致就是回文。因为首指针到了后半部分,走过的正好是尾指针走过的路,二者只是交换了位置,比较相等还是一样的。

import java.util.*;
public class Solution {public boolean judge (String str) {//首指针int left = 0;//尾指针int right = str.length() - 1;//首尾往中间靠while (left < right) {//比较前后是否相同if (str.charAt(left) != str.charAt(right))return false;left++;right--;}return true;}
}

反转字符串

//解法1
import java.util.*;
public class Solution {public String solve (String str) {char[] ans = str.toCharArray();int len = str.length();for(int i = 0 ; i < len ;i++){ans[i] = str.charAt(len-1-i);}return new String(ans);}
}
//解法2
import java.util.*;
public class Solution {public String solve (String str) {StringBuffer sb =new StringBuffer(str);//此方法针对的是io流,不能针对字符串。return sb.reverse().toString();}
}

最长无重复子数组

import java.util.*;public class Solution {/**** @param arr int整型一维数组 the array* @return int整型*/public int maxLength(int[] arr) {//用链表实现队列,队列是先进先出的Queue queue = new LinkedList<>();int res = 0;for (int c : arr) {while (queue.contains(c)) {//如果有重复的,队头出队queue.poll();}//添加到队尾queue.add(c);res = Math.max(res, queue.size());}return res;}
}

这里主要是要理解15行用while不用if和第21行代码,根据上面的用例输入就可以理解了,最后只要有重复的队列里的所有元素都会被弹出去并且只剩下最后一个元素,所以需要21行的代码每次记录最大值,用while也是为了弹出所有队列中的元素

解法二

具体做法

step 1:构建一个哈希表,用于统计数组元素出现的次数。

step 2:窗口左右界都从数组首部开始,每次窗口优先右移右界,并统计进入窗口的元素的出现频率。

step 3:一旦右界元素出现频率大于1,就需要右移左界直到窗口内不再重复,将左边的元素移除窗口的时候同时需要将它在哈希表中的频率减1,保证哈希表中的频率都是窗口内的频率。

step 4:每轮循环,维护窗口长度最大值。

import java.util.*;
public class Solution {public int maxLength (int[] arr) {//哈希表记录窗口内非重复的数字HashMap mp = new HashMap<>(); int res = 0;//设置窗口左右边界for(int left = 0, right = 0; right < arr.length; right++){ //窗口右移进入哈希表统计出现次数if(mp.containsKey(arr[right]))mp.put(arr[right],mp.get(arr[right])+1); elsemp.put(arr[right],1);//出现次数大于1,则窗口内有重复while(mp.get(arr[right]) > 1) //窗口左移,同时减去该数字的出现次数mp.put(arr[left],mp.get(arr[left++])-1); //维护子数组长度最大值res = Math.max(res, right - left + 1); }return res;}
}

贪心算法

盛水最多的容器

知识点1:双指针

双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。

知识点2:贪心思想

贪心思想属于动态规划思想中的一种,其基本原理是找出整体当中给的每个局部子结构的最优解,并且最终将所有的这些局部最优解结合起来形成整体上的一个最优解。

思路

这道题利用了水桶的短板原理,较短的一边控制最大水量,因此直接用较短边长乘底部两边距离就可以得到当前情况下的容积。但是要怎么找最大值呢?

可以利用贪心思想:我们都知道容积与最短边长和底边长有关,与长的底边一定以首尾为边,但是首尾不一定够高,中间可能会出现更高但是底边更短的情况,因此我们可以使用对撞双指针向中间靠,这样底边长会缩短,因此还想要有更大容积只能是增加最短变长,此时我们每次指针移动就移动较短的一边,因为贪心思想下较长的一边比较短的一边更可能出现更大容积。

//优先舍弃较短的边
if(height[left] < height[right]) left++;
elseright--;

具体做法

step 1:优先排除不能形成容器的特殊情况。

step 2:初始化双指针指向数组首尾,每次利用上述公式计算当前的容积,维护一个最大容积作为返回值。

step 3:对撞双指针向中间靠,但是依据贪心思想,每次指向较短边的指针向中间靠,另一指针不变。

import java.util.*;
public class Solution {public int maxArea (int[] height) {//排除不能形成容器的情况if(height.length < 2) return 0;int res = 0; //双指针左右界int left = 0; int right = height.length - 1;//共同遍历完所有的数组while(left < right){ //计算区域水容量int capacity = Math.min(height[left], height[right]) * (right - left); //维护最大值res = Math.max(res, capacity); //优先舍弃较短的边if(height[left] < height[right]) left++;elseright--;}return res;}
}

合并区间

具体做法

step 1:既然要求重叠后的区间按照起点位置升序排列,我们就将所有区间按照起点位置先进行排序。使用sort函数进行排序,重载比较方式为比较interval结构的start变量。

step 2:排序后的第一个区间一定是起点值最小的区间,我们将其计入返回数组res,然后遍历后续区间。

step 3:后续遍历过程中,如果遇到起点值小于res中最后一个区间的末尾值的情况,那一定是重叠,取二者最大末尾值更新res中最后一个区间即可。

step 4:如果遇到起点值大于res中最后一个区间的末尾值的情况,那一定没有重叠,后续也不会有这个末尾的重叠区间了,因为后面的起点只会更大,因此可以将它加入res。

import java.util.*;
public class Solution {public ArrayList merge(ArrayList intervals) {ArrayList res = new ArrayList<>();//去除特殊情况if(intervals.size() == 0) return res;//重载比较,按照区间首排序Collections.sort(intervals, new Comparator(){public int compare(Interval o1, Interval o2){if(o1.start != o2.start)return o1.start - o2.start;elsereturn o1.end - o2.end;}}); //放入第一个区间res.add(intervals.get(0)); int count = 0;//遍历后续区间,查看是否与末尾有重叠for(int i = 1; i < intervals.size(); i++){Interval o1 = intervals.get(i);Interval origin = res.get(count);if(o1.start > origin.end){res.add(o1);count++;//区间有重叠,更新结尾}else{ res.remove(count);Interval s = new Interval(origin.start, o1.end);if(o1.end < origin.end)s.end = origin.end;res.add(s);}}return res;}
}

分糖果问题

知识点:贪心思想

贪心思想属于动态规划思想中的一种,其基本原理是找出整体当中给的每个局部子结构的最优解,并且最终将所有的这些局部最优解结合起来形成整体上的一个最优解。

思路

要想分出最少的糖果,利用贪心思想,肯定是相邻位置没有增加的情况下,大家都分到1,相邻位置有增加的情况下,分到糖果数加1就好。什么情况下会增加糖果,相邻位置有得分差异,可能是递增可能是递减,如果是递增的话,糖果依次加1,如果是递减糖果依次减1?这不符合最小,因为减到最后一个递减的位置可能不是1,必须从1开始加才是最小,那我们可以从最后一个递减的位置往前反向加1.

具体做法

step 1:使用一个辅助数组记录每个位置的孩子分到的糖果,全部初始化为1.

step 2:从左到右遍历数组,如果右边元素比相邻左边元素大,意味着在递增,糖果数就是前一个加1,否则保持1不变。

step 3:从右到左遍历数组,如果左边元素比相邻右边元素大, 意味着在原数组中是递减部分,如果左边在上一轮中分到的糖果数更小,则更新为右边的糖果数+1,否则保持不变。

step 4:将辅助数组中的元素累加求和。

import java.util.*;
public class Solution {public int candy (int[] arr) {int n=arr.length;if(n<=1)return n;int[] nums = new int[n];//初始化for(int i = 0; i < n; i++)nums[i] = 1;//从左到右遍历for(int i = 1; i < arr.length; i++){ //如果右边在递增,每次增加一个if(arr[i] > arr[i - 1]) nums[i] = nums[i - 1] + 1; }//记录总糖果数int res = nums[arr.length - 1]; //从右到左遍历for(int i = arr.length - 2; i >= 0; i--){ //如果左边更大但是糖果数更小if(arr[i] > arr[i + 1] && nums[i] <= nums[i + 1])nums[i] = nums[i + 1] + 1; //累加和res += nums[i]; }return res;}
}

主持人调度

思路

我们利用贪心思想,什么时候需要的主持人最少?那肯定是所有的区间没有重叠,每个区间首和上一个的区间尾都没有相交的情况,我们就可以让同一位主持人不辞辛劳,一直主持了。但是题目肯定不是这种理想的情况,那我们需要对交叉部分,判断需要增加多少位主持人。

具体做法

step 1: 利用辅助数组获取单独各个活动开始的时间和结束时间,然后分别开始时间和结束时间进行排序,方便后面判断是否相交。

step 2: 遍历nnn个活动,如果某个活动开始的时间大于之前活动结束的时候,当前主持人就够了,活动结束时间往后一个。

step 3: 若是出现之前活动结束时间晚于当前活动开始时间的,则需要增加主持人。

import java.util.*;
public class Solution {public int minmumNumberOfHost (int n, int[][] startEnd) {int[] start = new int[n];int[] end = new int[n];//分别得到活动起始时间for(int i = 0; i < n; i++){start[i] = startEnd[i][0];end[i] = startEnd[i][1];}//单独排序Arrays.sort(start, 0, start.length);Arrays.sort(end, 0, end.length);int res = 0;int j = 0;for(int i = 0; i < n; i++){//新开始的节目大于上一轮结束的时间,主持人不变if(start[i] >= end[j]) j++;  else//主持人增加res++;  }return res;}
}

动态规划

斐波那契数列

知识点:动态规划

动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。

思路

斐波那契数列初始化第1项与第2项都是1,则根据公式第0项为0,可以按照斐波那契公式累加到第n项。

具体做法

step 1:低于2项的数列,直接返回n。

step 2:初始化第0项,与第1项分别为0,1.

step 3:从第2项开始,逐渐按照公式累加,并更新相加数始终为下一项的前两项。

public class Solution {public int Fibonacci(int n) {//从0开始,第0项是0,第一项是1if(n <= 1)    return n;int res = 0;int a = 0;int b = 1;//因n=2时也为1,初始化的时候把a=0,b=1for (int i = 2; i <= n; i++){//第三项开始是前两项的和,然后保留最新的两项,更新数据相加res = (a + b);a = b;b = res;}return res;}
}

跳台阶

知识点:递归

递归是一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。因此递归过程,最重要的就是查看能不能讲原本的问题分解为更小的子问题,这是使用递归的关键。

思路

一只青蛙一次可以跳1阶或2阶,直到跳到第nnn阶,也可以看成这只青蛙从n阶往下跳,到0阶,按照原路返回的话,两种方法事实上可以的跳法是一样的——即怎么来的,怎么回去! 当青蛙在第n阶往下跳,它可以选择跳1阶到n−1,也可以选择跳2阶到n−2,即它后续的跳法变成了f(n−1)+f(n−2),这就变成了斐波那契数列。因此可以按照斐波那契数列的做法来做:即输入n,输出第n个斐波那契数列的值,初始化0阶有1种,1阶有1种。

具体做法

step 1:低于2项的数列,直接返回n。

step 2:对于当前n,递归调用函数计算两个子问题相加。

public class Solution {public int jumpFloor(int target) {//这里第0项为1,第1项为1if(target <= 1)return 1;else//递归子问题相加return jumpFloor(target - 1) + jumpFloor(target - 2); }
}

跳台阶和斐波那契数列的答案是一样的,可以用动态规划也可以用递归实现

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...