题意:反转一个单链表。
示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL
思路步骤:
首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。
最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。
public class Solution {
public ListNode ReverseList(ListNode head) {
//pre指针:用来指向反转后的节点,初始化为null
ListNode pre = null;
//当前节点指针
ListNode cur = head;
//循环迭代
while(cur!=null){
//temp节点,永远指向当前节点cur的下一个节点
ListNode temp = cur.next;
//反转的关键:当前的节点指向其前一个节点(注意这不是双向链表,没有前驱指针)
cur.next = pre;
//更新pre
pre = cur;
//更新当前节点指针
cur = temp ;
}
//为什么返回pre?因为pre是反转之后的头节点
return pre;
}
}
思路步骤:
构建一个虚拟结点,让它指向原链表的头结点。
设置两个指针,pre 指针指向以虚拟头结点为链表的头部位置,cur 指针指向原链表的头部位置。
让着两个指针向前移动,直到 pre 指向了第一个要反转的结点的前面那个结点,而 cur 指向了翻转区域里面的第一个结点。
开始指向翻转操作
3)、让 temp 的 next 位置变成 cur
4)、让 pre 的 next 位置变成 temp
public class Solution {
public ListNode reverseBetween (ListNode head, int m, int n) {
if(head == null || head.next == null){
return head;
}
//设置虚拟头节点
ListNode dummy = new ListNode(-1);
dummy.next = head;
//走m-1步到 m的前一个节点
ListNode pre = dummy;
for(int i =0; i < m - 1; ++i){
pre = pre.next;
}
//走n-m+1步到 n的节点
ListNode rightNode = pre;
for(int i = 0; i < n-m+1; ++i){
rightNode = rightNode.next;
}
//截取出子链表
ListNode leftNode = pre.next;
ListNode cur = rightNode.next;
//断开链表
pre.next = null;
rightNode.next = null;
//反转中间的链表
reverseListNode(leftNode);
//拼接链表(这时已经反转过来了哦)
pre.next = rightNode;
leftNode.next = cur;
return dummy.next;
}
private void reverseListNode(ListNode head){
ListNode pre = null;
ListNode cur = head;
while(cur!=null){
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
}
}
思路步骤:
现在我们想一想,如果拿到一个链表,想要像上述一样分组翻转应该做些什么?首先肯定是分段吧,至少我们要先分成一组一组,才能够在组内翻转,之后就是组内翻转,最后是将反转后的分组连接。
但是连接的时候遇到问题了:首先如果能够翻转,链表第一个元素一定是第一组,它翻转之后就跑到后面去了,而第一组的末尾元素才是新的链表首,我们要返回的也是这个元素,而原本的链表首要连接下一组翻转后的头部,即翻转前的尾部,如果不建立新的链表,看起来就会非常难。但是如果我们从最后的一个组开始翻转,得到了最后一个组的链表首,是不是可以直接连在倒数第二个组翻转后的尾(即翻转前的头)后面,这样从后往前是不是看起来就容易多了。
怎样从后往前呢?我们这时候可以用到自上而下再自下而上的递归或者说栈。接下来我们说说为什么能用递归?如果这个链表有n个分组可以反转,我们首先对第一个分组反转,那么是不是接下来将剩余n−1个分组反转后的结果接在第一组后面就行了,那这剩余的n−1组就是一个子问题。我们来看看递归的三段式模版:
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head ListNode类
* @param k int整型
* @return ListNode类
*/
public ListNode reverseKGroup (ListNode head, int k) {
// write code here
//找到每次要反转的尾部;
ListNode tail = head;
//遍历k次到尾部
for(int i = 0; i < k; ++i){
//basecase
if(tail == null){
return head;
}
tail = tail.next;
}
//定义一个前驱节点和当前节点
ListNode pre = null;
ListNode cur = head;
//开始反转
while(cur != tail){
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
//当前尾指向下一个要反转的链表
head.next = reverseKGroup(tail,k);
//返回pre
return pre;
}
}
思路步骤:
list
的头结点后面接merge
好的链表(进入递归了);next
与另一结点merge
好的表头就ok了;重新整理一下:
list1.val <= list2.val
将较小的list1.next
与merge后的表头连接,即list1.next = Merge(list1.next,list2);
list2.val
较大时同理;复杂度:O(m+n)
O(m+n)
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param list1 ListNode类
* @param list2 ListNode类
* @return ListNode类
*/
public ListNode Merge (ListNode list1, ListNode list2) {
// write code here
//basecase
//要是list1为空,直接返回list2,同理
if(list1 == null){
return list2;
}else if(list2 == null){
return list1;
}
//递归
if(list1.val < list2.val){
list1.next = Merge(list1.next,list2);
return list1;
}else{
list2.next = Merge(list1,list2.next);
return list2;
}
}
}
思路步骤:
1、将两个链表合并。(递归方法合并)
2、分而治之!求一个mid,将mid左边的合并,右边的合并,最后将左右两边的链表合并。
3、重复这一过程,直到获取最终的有序链表。
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param lists ListNode类ArrayList
* @return ListNode类
*/
public ListNode mergeKLists (ArrayList<ListNode> lists) {
// write code here
//递归
return mergeList(lists, 0 ,lists.size() - 1);
}
private ListNode mergeList(ArrayList<ListNode> lists, int left, int right) {
//basecase
if(left == right){
return lists.get(left);
}
if(left > right){
return null;
}
int mid = left + ((right - left)>>1);
return merge(mergeList(lists, left, mid),mergeList(lists, mid + 1,right));
}
//合并两个有序链表
private ListNode merge(ListNode list1, ListNode list2){
//basecase
if(list1 == null){
return list2;
}if(list2 == null){
return list1;
}
if(list1.val < list2.val){
list1.next = merge(list1.next, list2);
return list1;
}else{
list2.next = merge(list1, list2.next);
return list2;
}
}
}
思路步骤:
我们都知道链表不像二叉树,每个节点只有一个val值和一个next指针,也就是说一个节点只能有一个指针指向下一个节点,不能有两个指针,那这时我们就可以说一个性质:环形链表的环一定在末尾,末尾没有NULL了。为什么这样说呢?仔细看上图,在环2,0,-4中,没有任何一个节点可以指针指出环,它们只能在环内不断循环,因此环后面不可能还有一条尾巴。如果是普通线形链表末尾一定有NULL,那我们可以根据链表中是否有NULL判断是不是有环。
但是,环形链表遍历过程中会不断循环,线形链表遍历到NULL结束了,但是环形链表何时能结束呢?我们可以用双指针技巧,同向访问的双指针,速度是快慢的,只要有环,二者就会在环内不断循环,且因为有速度差异,二者一定会相遇。
具体做法:
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;
}
}
题目描述:
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
思路步骤:
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead) {
/**
快慢指针
*/
//basecase
if(pHead == null && pHead.next == null){
return pHead;
}
//定义两个指针
ListNode fast = pHead;
ListNode slow = pHead;
while (fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
ListNode slow2 = pHead;
while(slow2 != slow){
slow2 = slow2.next;
slow = slow.next;
}
return slow;
}
}
return null;
}
}
思路步骤:
这题要求链表的倒数第k个节点,最简单的方式就是使用两个指针
第一个指针先移动k步,然后第二个指针再从头开始,这个时候这两个指针同时移动,当第一个指针到链表的末尾的时候,返回第二个指针即可。
注意,如果第一个指针还没走k步的时候链表就为空了,我们直接返回null
即可。
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead ListNode类
* @param k int整型
* @return ListNode类
*/
public ListNode FindKthToTail (ListNode pHead, int k) {
// write code here
//双指针
//basecase
if(pHead == null){
return pHead;
}
//首先定义两个指针
ListNode first = pHead;
ListNode second = pHead;
//让first指针先走k步
while(k > 0){
//如果first还没走到k步链表就为空,直接返回null
if(first == null){
return null;
}
first = first.next;
k--;
}
//然后两个指针一起走
while(first != null){
first = first.next;
second = second.next;
}
//返回second节点
return second;
}
}
思路步骤:
我们可以使用两个指针,一个指针fast
先走n
步,然后另一个指针slow
从头结点开始,找到要删除结点的前一个结点,这样就可以完成结点的删除了。
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head ListNode类
* @param n int整型
* @return ListNode类
*/
public ListNode removeNthFromEnd (ListNode head, int n) {
// 双指针解法
// 首先定义两个指针
ListNode fast = head;
ListNode slow = head;
//fast要移动n步slow才能移动
for(int i = 0; i < n; i++){
fast = fast.next;
}
//basecase
if(fast == null){
return head.next;
}
//开始走啦
while(fast.next != null){
fast = fast.next;
slow = slow.next;
}
//现在slow走到了n的前一个节点,直接跳过n节点,链接到下一个节点
slow.next = slow.next.next;
//返回head
return head;
}
}
思路步骤:
我们准备两个指针分别从两个链表头同时出发,每次都往后一步,遇到末尾就连到另一个链表的头部,这样相当于每个指针都遍历了这个交叉链表的所有结点,那么它们相遇的地方一定是交叉的地方,即第一个公共结点。
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
//首先定义两个指针
ListNode first = pHead1;
ListNode second = pHead2;
//当两个指针不相遇的时候就一直遍历
while(first != second){
first = (first == null )? pHead2 : first.next;
second = (second == null)? pHead1 : second.next;
}
//返回任意一个
return first;
}
}
思路步骤:
既然链表每个节点表示数字的每一位,那相加的时候自然可以按照加法法则,从后往前依次相加。但是,链表是没有办法逆序访问的,这是我们要面对第一只拦路虎。解决它也很简单,既然从后往前不行,那从前往后总是可行的吧,将两个链表反转一 下,即可得到个十百千……各个数字从前往后的排列,相加结果也是个位在前,怎么办?再次反转,结果不就正常了。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head1 ListNode类
* @param head2 ListNode类
* @return ListNode类
*/
public ListNode addInList (ListNode head1, ListNode head2) {
//basecase
//任意一个链表为空,直接返回另外一个
if (head1 == null)
return head2;
if (head2 == null)
return head1;
//反转两个链表
head1 = reverseList(head1);
head2 = reverseList(head2);
//添加一个表头
ListNode dummy = new ListNode(-1);
ListNode head = dummy;
//定义一个变量用来存放是否要进位
int carry = 0;
//只要某个链表还有或者进位还有
while (head1 != null || head2 != null || carry != 0) {
//链表不为空则取其值
int val1 = head1 == null ? 0 : head1.val;
int val2 = head2 == null ? 0 : head2.val;
//相加
int temp = val1 + val2 + carry;
//获取进位
carry = temp / 10;
//获取结果值
temp %= 10;
//添加元素
head.next = new ListNode(temp);
head = head.next;
//移动下一个
if (head1 != null) {
head1 = head1.next;
}
if (head2 != null) {
head2 = head2.next;
}
}
//结果反转回来
return reverseList(dummy.next);
}
//反转两个链表
public ListNode reverseList(ListNode head) {
//basecase
if (head == null) {
return null;
}
//定义两个节点
ListNode pre = null;
ListNode cur = head;
//反转链表
while (cur != null) {
//断开链表,要记录后续一个
ListNode temp = cur.next;
//当前的next指向前一个
cur.next = pre;
//前一个更新为当前
pre = cur;
//当前记录为刚刚记录的后一个
cur = temp;
}
return pre;
}
}
思路步骤:
堆排序应该是最简单直观的,且时间复杂度和空间复杂度都符合题目要求。注意点就是最后一个节点的next指针要设置为null,否则可能会出现环形链表的情况
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head ListNode类 the head node
* @return ListNode类
*/
public ListNode sortInList (ListNode head) {
// 堆排序
PriorityQueue<ListNode> heap = new PriorityQueue<>((n1, n2) -> n1.val - n2.val);
while(head != null){
//将元素全部丢到堆里
heap.add(head);
//链表后移
head = head.next;
}
//定义一个虚拟头节点
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
//将堆里面的数据全部拿出来
while(!heap.isEmpty()){
cur.next = heap.poll();
cur = cur.next;
}
cur.next = null;
return dummy.next;
}
}
思路步骤:
这题是让判断链表是否是回文链表,所谓的回文链表就是以链表中间为中心点两边对称。我们常见的有判断一个字符串是否是回文字符串,这个比较简单,可以使用两个指针,一个最左边一个最右边,两个指针同时往中间靠,判断所指的字符是否相等
。
但这题判断的是链表,因为这里是单向链表,只能从前往后访问,不能从后往前访问,所以使用判断字符串的那种方式是行不通的。但我们可以通过找到链表的中间节点然后把链表后半部分反转,最后再用后半部分反转的链表和前半部分一个个比较即可。
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head ListNode类 the head
* @return bool布尔型
*/
public boolean isPail (ListNode head) {
//快慢指针
//首先定义两个指针
ListNode fast = head;
ListNode slow = head;
//接着走,直到快指针为null,并且快指针的下一个为null
while(fast != null && fast.next != null){
//快指针走两步,满指针走一步
fast = fast.next.next;
slow = slow.next;
}
//链表是奇数的情况下,将慢指针再往后走一步
if(fast != null){
slow = slow.next;
}
//然后反转慢指针所在的往后的链表
slow = reverseList(slow);
//将快指针指向头节点
fast = head;
//将快指针和慢指针对应的值进行比较
while(slow != null){
//若是值不相等,返回false
if(fast.val != slow.val){
return false;
}
//快慢指针各走一步
fast = fast.next;
slow = slow.next;
}
//返回true
return true;
}
//反转链表
private ListNode reverseList(ListNode head){
//要是这个链表为空,直接返回这个链表
if(head == null){
return head;
}
//然后定义两个指针开始反转
ListNode pre = null;
ListNode cur = head;
while(cur != null){
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
//返回pre
return pre;
}
}
思路步骤:
如下图所示,第一个节点是奇数位,第二个节点是偶数,第二个节点后又是奇数位,因此可以断掉节点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;
这样我们就可以使用了两个同方向访问指针遍历解决这道题。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head ListNode类
* @return ListNode类
*/
public ListNode oddEvenList (ListNode head) {
// 判断空链表的情况,如果链表为空,直接返回
if(head == null){
return head;
}
//使用双指针odd和even分别遍历奇数节点和偶数节点
//首次定义两个指针
ListNode odd = head;
ListNode even = head.next;
//给偶数节点链表一个头
ListNode evenhead = even;
//开始遍历
while(even != null && even.next != null){
//odd连接even的后一个,即奇数位
odd.next = even.next;
//odd向后走一位
odd = odd.next;
//even连接odd的后一个,即偶数位
even.next = odd.next;
//even向后走一位
even = even.next;
}
//even整体接在odd后面
odd.next = evenhead;
return head;
}
}
思路步骤:
既然连续相同的元素只留下一个,我们留下哪一个最好呢?当然是遇到的第一个元素了!
if(cur.val == cur.next.val)
cur.next = cur.next.next;
因为第一个元素直接就与前面的链表节点连接好了,前面就不用管了,只需要跳过后面重复的元素,连接第一个不重复的元素就可以了,在链表中连接后面的元素总比连接前面的元素更方便嘛,因为不能逆序访问。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head ListNode类
* @return ListNode类
*/
public ListNode deleteDuplicates (ListNode head) {
// write code here
/**
* 当发现有相同的结点则删除,cur.next = cur.next.next
* 其他情况下,继续循环:cur = cur.next
*/
//basecase
if(head == null){
return null;
}
//遍历指针
ListNode cur = head;
//遍历链表
while(cur != null && cur.next != null){
if(cur.next != null && cur.val == cur.next.val){
//跳过那个节点
cur.next = cur.next.next;
}else{
//否则指针正常遍历
cur = cur.next;
}
}
return head;
}
}
题目描述:
思路步骤:
这是一个升序链表,重复的节点都连在一起,我们就可以很轻易地比较到重复的节点,然后将所有的连续相同的节点都跳过,连接不相同的第一个节点。
//遇到相邻两个节点值相同
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;
}
具体做法:
ListNode res = new ListNode(0);
//在链表前加一个表头
res.next = head;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head ListNode类
* @return ListNode类
*/
public ListNode deleteDuplicates (ListNode head) {
// write code here
//空链表
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){
//将cur.next.val存到一个临时变量
int temp = cur.next.val;
//将所有相同的都跳过
while(cur.next != null && cur.next.val == temp){
cur.next = cur.next.next;
}
}else{
//指针往后
cur = cur.next;
}
}
//返回时去掉表头
return res.next;
}
}
思路步骤:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param nums int整型一维数组
* @param target int整型
* @return int整型
*/
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;
}else{
l = m + 1;
}
}
//未找到
return -1;
}
}
思路步骤:
似乎我们可以直接从上到下遍历矩阵,再从左到右遍历矩阵每一行,然后检验目标值是否是遇到的元素。
//两层循环,遍历二维数组
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
//找到target
if(array[i][j] == target)
return true;
但是我们这样就没有利用到矩阵内部的行列都是有序这个性质,我们再来找找规律:
首先看四个角,左上与右下必定为最小值与最大值,而左下与右上就有规律了:左下元素大于它上方的元素,小于它右方的元素,右上元素与之相反
。既然左下角元素有这么一种规律,相当于将要查找的部分分成了一个大区间和小区间,每次与左下角元素比较,我们就知道目标值应该在哪部分中,于是可以利用分治思维来做。
具体做法:
图示:
public class Solution {
public boolean Find (int target, int[][] array) {
// 二分查找
//basecase
//首先判断矩阵的两个边长
if(array.length == 0){
return false;
}
if(array[0].length == 0){
return false;
}
//定义两个变量
int m = array.length;
int n = array[0].length;
//从最左下角开始往右或者往上找
for(int i = 0, j = n -1; i < m && j >= 0;){
//小于目标值,开始往右找
if(array[i][j] < target){
i++;
}else if(array[i][j] > target){
j--;
}else{
return true;
}
}
return false;
}
}
思路:
因为题目将数组边界看成最小值,而我们只需要找到其中一个波峰,因此只要不断地往高处走,一定会有波峰。那我们可以每次找一个标杆元素,将数组分成两个区间,每次就较高的一边走,因此也可以用分治来解决,而标杆元素可以选择区间中点。
//右边是往下,不一定有坡峰
if(nums[mid] > nums[mid + 1])
right = mid;
//右边是往上,一定能找到波峰
else
left = mid + 1;
具体做法:
图示:
public class Solution {
public int findPeakElement (int[] nums) {
//关键思想:下坡的时候可能找到波峰,但是可能找不到,一直向下走的
//上坡的时候一定能找到波峰,因为题目给出的是nums[-1] = nums[n] = -∞
int l = 0;
int r = nums.length - 1;
while(l < r){
//int mid = l + ((l - r)>>1);
int mid = (l + r) / 2;
//右边是往下,不一定有波峰
if(nums[mid] > nums[mid + 1]){
r = mid;
}else{
l = mid + 1;
}
}
return r;
}
}
思路步骤
那么,我们先来说说归并算法吧,归并算法讲究一个先分后并!
先分:分呢,就是将数组分为两个子数组,两个子数组分为四个子数组,依次向下分,直到数组不能再分为止!
后并:并呢,就是从最小的数组按照顺序合并,从小到大或从大到小,依次向上合并,最后得到合并完的顺序数组!
介绍完归并排序,我们来说说归并统计法,我们要在哪个步骤去进行统计呢?
归并统计法,关键点在于合并环节,在合并数组的时候,当发现右边的小于左边的时候,此时可以直接求出当前产生的逆序对的个数。
举个例子:
在合并 {4 ,5} {1 , 2} 的时候,首先我们判断 1 < 4,我们即可统计出逆序对为2,为什么呢?这利用了数组的部分有序性。因为我们知道 {4 ,5} 这个数组必然是有序的,因为是合并上来的。此时当 1比4小的时候,证明4以后的数也都比1大,此时就构成了从4开始到 {4,5}这个数组结束,这么多个逆序对(2个),此时利用一个临时数组,将1存放起来,接着比较2和4的大小,同样可以得到有2个逆序对,于是将2也放进临时数组中,此时右边数组已经完全没有元素了,则将左边剩余的元素全部放进临时元素中,最后将临时数组中的元素放进原数组对应的位置。
最后接着向上合并~
可以看到下面这张图~
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param nums int整型一维数组
* @return int整型
*/
int count = 0;
public int InversePairs (int[] nums) {
// 归并排序
//长度小于2则无逆序对
if (nums == null || nums.length < 2) {
return 0;
}
//将数组分为左右两部分进行排序
mergeSort(nums, 0, nums.length - 1);
return count;
}
private void mergeSort(int[] arr, int L, int R) {
//basecase
//如果L下标和R下标重合,那也说明有序
if (L == R) {
return;
}
//找分割点
int mid = L + ((R - L) >> 1);
//将 左边部分排好序
mergeSort(arr, L, mid);
//将 右边部分排好序
mergeSort(arr, mid + 1, R);
//merge,将两部分排好序的数组进行整体排序
merge(arr, L, mid, R);
}
public void merge(int[] arr, int L, int mid, int R) {
//首先定义一个help数组,长度为此时两个子数组加起来的长度
int[] help = new int[R - L + 1];
//临时数组的下标起点
int i = 0;
//定义两个指针
int p1 = L;
int p2 = mid + 1;
//如果这两个数组都没有越界
while (p1 <= mid && p2 <= R) {
// 当左子数组的当前元素小的时候,跳过,无逆序对
if (arr[p1] <= arr[p2]) {
// 放入临时数组
help[i++] = arr[p1++];
} else { // 否则,此时存在逆序对
// 放入临时数组
help[i++] = arr[p2++];
// 逆序对的个数为 左子数组的终点- 当前左子数组的当前指针
count += mid - p1 + 1;
count %= 1000000007;
}
//help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
//如果左边的数组没有越界
while (p1 <= mid) {
//将左边数组的值直接拷贝到hele数组里面
help[i++] = arr[p1++];
}
//如果右边的数组没有越界
while (p2 <= R) {
//将右边数组的值直接拷贝到hele数组里面
help[i++] = arr[p2++];
}
//经过上面的步骤,help数组有序啦,拷贝到arr数组就完成啦
for (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
}
解题思路:
排序数组的查找问题首先考虑使用 二分法 解决,其可将 遍历法 的 线性级别 时间复杂度降低至 对数级别
算法流程:
1、初始化: 声明 i, j 双指针分别指向 array 数组左右两端
2、循环二分: 设 m = (i + j) / 2 为每次二分的中点( “/” 代表向下取整除法,因此恒有 i≤m1、当 array[m] > array[j] 时: m 一定在 左排序数组 中,即旋转点 x 一定在 [m + 1, j] 闭区间内,因此执行 i = m + 1
3、当 array[m] < array[j] 时: m 一定在 右排序数组 中,即旋转点 x 一定在[i, m]闭区间内,因此执行 j = m
4、当 array[m] = array[j] 时: 无法判断 mm 在哪个排序数组中,即无法判断旋转点 x 在 [i, m] 还是 [m + 1, j] 区间中。
解决方案: 执行 j = j - 1 缩小判断范围
5、返回值: 当 i = j 时跳出二分循环,并返回 旋转点的值 array[i] 即可。
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param nums int整型一维数组
* @return int整型
*/
public int minNumberInRotateArray (int[] nums) {
// basecase
if(nums.length == 0){
return 0;
}
//左右指针
int l = 0 , r = nums.length - 1;
//循环
while(l < r){
//找到数组中的重点m
int m = l + (r - l)/2;
// m在左排序数组中,旋转点在 [m+1, r] 中
if(nums[m] > nums[r]){
l = m + 1;
// m 在右排序数组中,旋转点在 [l, m]中
}else if(nums[m] < nums[r]){
r = m;
}else{
// 缩小范围继续判断
r--;
}
}
// 返回旋转点
return nums[l];
}
}
思路:
既然是比较两个字符串每个点之间的数字是否相同,就直接同时遍历字符串比较,因此我们需要使用两个同向访问的指针各自访问一个字符串。
比较的时候,数字前导零不便于我们比较,因为我们不知道后面会出现多少前导零,因此应该将点之间的部分转化为数字再比较才方便。
while(i < n1 && version1[i] != '.'){
num1 = num1 * 10 + (version1[i] - '0');
i++;
}
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 比较版本号
* @param version1 string字符串
* @param version2 string字符串
* @return int整型
*/
public int compare (String version1, String version2) {
// write code here
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;
}
}
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param root TreeNode类
* @return int整型一维数组
*/
public int[] preorderTraversal (TreeNode root) {
//前序遍历 遵循根--->左--->右
//创建一个集合来存储遍历的结点值
List<Integer> list = new ArrayList<>();
helper(root, list);
int[] res = new int[list.size()];
for (int i = 0; i < res.length; i++) {
//将集合中的元素转存到数组中
res[i] = list.get(i);
}
//将数组返回
return res;
}
private void helper(TreeNode node, List<Integer> list) {
if (node == null) {
return;
}
//存入结点
list.add(node.val);
//遍历左树
helper(node.left, list);
//遍历右树
helper(node.right, list);
}
}
思路:
什么是二叉树的中序遍历,简单来说就是“左根右”,展开来说就是对于一棵二叉树,我们优先访问它的左子树,等到左子树全部节点都访问完毕,再访问根节点,最后访问右子树。同时访问子树的时候,顺序也与访问整棵树相同。
从上述对于中序遍历的解释中,我们不难发现它存在递归的子问题,根节点的左右子树访问方式与原本的树相同,可以看成一颗树进行中序遍历,因此可以用递归处理:
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param root TreeNode类
* @return int整型一维数组
*/
public int[] inorderTraversal (TreeNode root) {
// write code here
/**
递归法
*/
//中序遍历 遵循左--->根--->右
//创建一个集合来存储遍历的结点值
List<Integer> list = new ArrayList<>();
helper(root, list);
int[] res = new int[list.size()];
for (int i = 0; i < res.length; i++) {
//将集合中的元素转存到数组中
res[i] = list.get(i);
}
//将数组返回
return res;
}
private void helper(TreeNode node, List<Integer> list) {
if (node == null) {
return;
}
//遍历左树
helper(node.left, list);
//存入结点
list.add(node.val);
//遍历右树
helper(node.right, list);
}
}
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param root TreeNode类
* @return int整型一维数组
*/
public int[] postorderTraversal (TreeNode root) {
// write code here
//后序遍历 遵循左--->右--->根
//创建一个集合来存储遍历的结点值
List<Integer> list = new ArrayList<>();
helper(root, list);
int[] res = new int[list.size()];
for (int i = 0; i < res.length; i++) {
//将集合中的元素转存到数组中
res[i] = list.get(i);
}
//将数组返回
return res;
}
private void helper(TreeNode node, List<Integer> list) {
//basecase
if (node == null) {
return;
}
helper(node.left, list);
helper(node.right, list);
list.add(node.val);
}
}
BFS广度优先遍历
思路:
二叉树的层次遍历就是按照从上到下每行,然后每行中从左到右依次遍历,得到的二叉树的元素值。对于层次遍历,我们通常会使用队列来辅助:
因为队列是一种先进先出的数据结构,我们依照它的性质,如果从左到右访问完一行节点,并在访问的时候依次把它们的子节点加入队列,那么它们的子节点也是从左到右的次序,且排在本行节点的后面,因此队列中出现的顺序正好也是从左到右,正好符合层次遍历的特点。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param root TreeNode类
* @return int整型ArrayList>
*/
public ArrayList<ArrayList<Integer>> levelOrder (TreeNode root) {
//BFS 广度优先遍历
//通常与队列一起配合
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
//basecase
if (root == null) {
return res;
}
//定义一个队列
Queue<TreeNode> q = new LinkedList<>();
//将root 先丢到队列里
q.add(root);
//开启新的层
while (q.size() > 0) {
//维护一个size用来记录队列中元素的数量
//每次循环都更新size
int size = q.size();
//定义一个List用来记录二叉树的某一行
ArrayList<Integer> list = new ArrayList<>();
//当前层的元素
while (size > 0) {
//将队列中的元素取出来
TreeNode cur = q.poll();
//存到List
list.add(cur.val);
//判断取出的这个元素有没有左右孩子
if (cur.left != null) {
//将左孩子加到队列
q.add(cur.left);
}
if (cur.right != null) {
q.add(cur.right);
}
size--;
}
res.add(new ArrayList<>(list));
}
return res;
}
}
DFS深度优先遍历
思路:
既然二叉树的前序、中序、后序遍历都可以轻松用递归实现,树型结构本来就是递归喜欢的形式,那我们的层次遍历是不是也可以尝试用递归来试试呢?
按行遍历的关键是每一行的深度对应了它输出在二维数组中的深度,即深度可以与二维数组的下标对应,那我们可以在递归的访问每个节点的时候记录深度:
dfs(root, res, 0);
进入子节点则深度加1:
//递归左右时深度记得加1
dfs(node.left, res, level + 1);
dfs(node.right, res, level + 1);
每个节点值放入二维数组相应行。
res.get(level).add(node.val);
因此可以用递归实现:
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param root TreeNode类
* @return int整型ArrayList>
*/
public ArrayList<ArrayList<Integer>> levelOrder (TreeNode root) {
//DFS 深度优先遍历
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
//basecase
if (root == null) {
return res;
}
dfs(root, res, 0);
return res;
}
private void dfs(TreeNode node, ArrayList<ArrayList<Integer>> res, int level) {
//递归退出的条件
if (node == null) {
return;
}
//当前层级大于数组大小
if (level > res.size() - 1) {
//就需要加一个空的数组,才能存放元素
res.add(new ArrayList<>());
}
res.get(level).add(node.val);
if (node.left != null) {
dfs(node.left, res, level + 1);
}
if (node.right != null) {
dfs(node.right, res, level + 1);
}
}
}
思路:
按照层次遍历按层打印二叉树的方式,每层分开打印,然后对于每一层利用flag标记,第一层为false,之后每到一层取反一次,如果该层的flag为true,则记录的数组整个反转即可。
//奇数行反转,偶数行不反转
if(flag)
reverse(row.begin(), row.end());
但是难点在于如何每层分开存储,从哪里知晓分开的时机?在层次遍历的时候,我们通常会借助队列(queue),事实上,队列中的值大有玄机,让我们一起来看看:当根节点进入队列时,队列长度为1,第一层节点数也为1;若是根节点有两个子节点,push进队列后,队列长度为2,第二层节点数也为2;若是根节点一个子节点,push进队列后,队列长度为为1,第二层节点数也为1。由此,我们可知,每层的节点数等于进入该层时队列长度,因为刚进入该层时,这一层每个节点都会push进队列,而上一层的节点都出去了。
int n = temp.size();
for(int i = 0; i < n; i++){
//访问一层
}
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pRoot TreeNode类
* @return int整型ArrayList>
*/
public ArrayList<ArrayList<Integer>> Print (TreeNode pRoot) {
// write code here
TreeNode head = pRoot;
ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
if(head == null){
//返回空list
return res;
}
//队列存储,进行层次遍历
Queue<TreeNode> temp = new LinkedList<TreeNode>();
//先把根节点加到队列里面
temp.offer(head);
//定义一个变量存放队列中取出来节点
TreeNode p;
//定义一个flag变量
boolean flag = true;
while(!temp.isEmpty()){
//定义一个数组记录二叉树的某一行
ArrayList<Integer> row = new ArrayList<>();
//维护一个变量记录队列元素的个数
int n = temp.size();
//奇数行反转,偶数行不反转
flag = !flag;
//因为首先进入的都是根节点,所以每层节点多少,队列大小就是多少
for(int i = 0; i < n; i++){
//将队列中的节点取出来
p = temp.poll();
//加到存放行的列表
row.add(p.val);
//若是左右孩子存在,则存入左右孩子作为下一个层次
if(p.left != null){
temp.offer(p.left);
}
if(p.right != null){
temp.offer(p.right);
}
}
//奇数行反转,偶数行不反转
if(flag){
Collections.reverse(row);
}
res.add(row);
}
return res;
}
}
方法一:递归
二叉树的递归,则是将某个节点的左子树、右子树看成一颗完整的树,那么对于子树的访问或者操作就是对于原树的访问或者操作的子问题,因此可以自我调用函数不断进入子树。
思路:
最大深度是所有叶子节点的深度的最大值,深度是指树的根节点到任一叶子节点路径上节点的数量,因此从根节点每次往下一层深度就会加1。因此二叉树的深度就等于根节点这个1层加上左子树和右子树深度的最大值,而每个子树我们都可以看成一个根节点,继续用上述方法求的深度,于是我们可以对这个问题划为子问题,利用递归来解决:
具体做法:
import java.util.*;
public class Solution {
public int maxDepth (TreeNode root) {
//空节点没有深度
if(root == null)
return 0;
//返回子树深度+1
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}
方法二:队列
队列是一种仅支持在表尾进行插入操作、在表头进行删除操作的线性表,插入端称为队尾,删除端称为队首,因整体类似排队的队伍而得名。它满足先进先出的性质,元素入队即将新元素加在队列的尾,元素出队即将队首元素取出,它后一个作为新的队首。
思路:
既然是统计二叉树的最大深度,除了根据路径到达从根节点到达最远的叶子节点以外,我们还可以分层统计。对于一棵二叉树而言,必然是一层一层的,那一层就是一个深度,有的层可能会很多节点,有的层如根节点或者最远的叶子节点,只有一个节点,但是不管多少个节点,它们都是一层。因此我们可以使用层次遍历,二叉树的层次遍历就是从上到下按层遍历,每层从左到右,我们只要每层统计层数即是深度。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param root TreeNode类
* @return int整型
*/
public int maxDepth (TreeNode root) {
//BFS
// write code here
if (root == null) {
return 0;
}
//创建一个队列
Queue<TreeNode> q = new LinkedList<>();
//将根节点加入到队列
q.offer(root);
//维护一个res记录深度
int res = 0;
//如果这个队列不等于空
while (!q.isEmpty()) {
//每一层的个数
int size = q.size();
while (size-- > 0) {
//如果这一层的个数大于0,就取出来
TreeNode cur = q.poll();
//取出来之后看这个节点是否有左右孩子
if (cur.left != null)
//加入到队列
q.offer(cur.left);
if (cur.right != null)
q.offer(cur.right);
}
//每一层的个数++
res++;
}
return res;
}
}
思路步骤:
既然是检查从根到叶子有没有一条等于目标值的路径,那肯定需要从根节点遍历到叶子,我们可以在根节点每次往下一层的时候,将sum减去节点值,最后检查是否完整等于0. 而遍历的方法我们可以选取二叉树常用的递归前序遍历,因为每次进入一个子节点,更新sum值以后,相当于对子树查找有没有等于新目标值的路径,因此这就是子问题,递归的三段式为:
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param root TreeNode类
* @param sum int整型
* @return bool布尔型
*/
public boolean hasPathSum (TreeNode root, int sum) {
// write code here
//如果根节点为空,或者叶子节点也遍历完了也没找到这样的结果,就返回false
if (root == null)
return false;
//如果到叶子节点了,并且剩余值等于叶子节点的值,说明找到了这样的结果,直接返回true
if (root.left == null && root.right == null && sum - root.val == 0)
return true;
//分别沿着左右子节点走下去,然后顺便把当前节点的值减掉,左右子节点只要有一个返回true,
//说明存在这样的结果
return hasPathSum(root.left, sum - root.val) ||
hasPathSum(root.right, sum - root.val);
}
}
思路:
二叉搜索树最左端的元素一定最小,最右端的元素一定最大,符合“左中右”的特性,因此二叉搜索树的中序遍历就是一个递增序列,我们只要对它中序遍历就可以组装称为递增双向链表。
具体做法:
public class Solution {
//返回的第一个指针,即为最小值,先定为null
TreeNode head = null;
//中序遍历当前值的上一位,初值为最小值,先定为null
TreeNode pre = null;
public TreeNode Convert(TreeNode pRootOfTree) {
if (pRootOfTree == null) {
return null;
}
//首先递归到最左最小值
Convert(pRootOfTree.left);
//找到最小值,然后初始化head和pre
if (pre == null) {
head = pRootOfTree;
pre = pRootOfTree;
}
//当前节点与上一节点建立连接,将pre设置为当前值
else {
pre.right = pRootOfTree;
pRootOfTree.left = pre;
pre = pRootOfTree;
}
Convert(pRootOfTree.right);
return head;
}
}
思路:
前序遍历的时候我们采用的是“根左右”的遍历次序,如果这棵二叉树是对称的,即相应的左右节点交换位置完全没有问题,那我们是不是可以尝试“根右左”遍历,按照轴对称图像的性质,这两种次序的遍历结果应该是一样的。
不同的方式遍历两次,将结果拿出来比较看起来是一种可行的方法,但也仅仅可行,太过于麻烦。我们不如在遍历的过程就结果比较了。而遍历方式依据前序递归可以使用递归:
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pRoot TreeNode类
* @return bool布尔型
*/
public boolean isSymmetrical (TreeNode pRoot) {
// write code here
return recursion(pRoot, pRoot);
}
public boolean recursion(TreeNode root1, TreeNode root2) {
//可以两个都为空,
if (root1 == null && root2 == null) {
return true;
}
//只有一个为空或者节点值不同,一定不对称
if (root1 == null || root2 == null || root1.val != root2.val) {
return false;
}
//每层对应的节点进入递归比较
return recursion(root1.left, root2.right) && recursion(root1.right, root2.left);
}
}
思路:
要将一棵二叉树的节点与另一棵二叉树相加合并,肯定需要遍历两棵二叉树,那我们可以考虑同步遍历两棵二叉树,这样就可以将每次遍历到的值相加在一起。遍历的方式有多种,这里推荐前序递归遍历。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param t1 TreeNode类
* @param t2 TreeNode类
* @return TreeNode类
*/
public TreeNode mergeTrees (TreeNode t1, TreeNode t2) {
// write code here
//若只有一个节点返回另一个,两个都为null自然返回null
if(t1 == null){
return t2;
}
if(t2 == null){
return t1;
}
//根左右的方式递归
TreeNode head = new TreeNode(t1.val + t2.val);
head.left = mergeTrees(t1.left, t2.left);
head.right = mergeTrees(t1.right,t2.right);
return head;
}
}
解题思路:
根据二叉树镜像的定义,考虑递归遍历(dfs)二叉树,交换每个节点的左 / 右子节点,即可生成二叉树的镜像。
解题步骤:
1、特判:如果pRoot为空,返回空
2、交换左右子树
3、把pRoot的左子树放到Mirror中镜像一下
4、把pRoot的右子树放到Mirror中镜像一下
5、返回根节点root
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pRoot TreeNode类
* @return TreeNode类
*/
public TreeNode Mirror (TreeNode pRoot) {
// write code here
if(pRoot == null){
return pRoot;
}
//左右子树交换
TreeNode temp = pRoot.left;
pRoot.left = pRoot.right;
pRoot.right = temp;
//递归左右子树
Mirror(pRoot.left);
Mirror(pRoot.right);
return pRoot;
}
}
思路:
二叉搜索树的特性就是中序遍历是递增序。既然是判断是否是二叉搜索树,那我们可以使用中序递归遍历。只要之前的节点是二叉树搜索树,那么如果当前的节点小于上一个节点值那么就可以向下判断。只不过在过程中我们要求反退出。比如一个链表1->2->3->4,只要for循环遍历如果中间有不是递增的直接返回false即可。
if(root.val < pre)
return false;
具体做法:
public class Solution {
int pre = Integer.MIN_VALUE;
public boolean isValidBST (TreeNode root) {
// write code here
//中序遍历
if(root == null){
return true;
}
//先进入左子树
if(!isValidBST(root.left)){
return false;
}
if(root.val < pre){
return false;
}
//更新最值
pre = root.val;
//再进入右子树
return isValidBST(root.right);
}
}
思路:
对完全二叉树最重要的定义就是叶子节点只能出现在最下层和次下层,所以我们想到可以使用队列辅助进行层次遍历——从上到下遍历所有层,每层从左到右,只有次下层和最下层才有叶子节点,其他层出现叶子节点就意味着不是完全二叉树。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param root TreeNode类
* @return bool布尔型
*/
public boolean isCompleteTree (TreeNode root) {
// write code here
//空树一定是完全二叉树
if(root == null){
return true;
}
//辅助队列
Queue<TreeNode> q = new LinkedList<>();
//根节点加入队列
q.offer(root);
//从队列中取出的节点
TreeNode cur;
//定义一个首次出现的标记位
boolean notComplete = false;
while(!q.isEmpty()){
cur = q.poll();
//标记第一次遇到空节点
if(cur == null){
notComplete = true;
//退出
continue;
}
//后续访问已经遇到空节点了
if(notComplete){
return false;
}
q.offer(cur.left);
q.offer(cur.right);
}
return true;
}
}
思路:
从题中给出的有效信息:
故此 首先想到的方法是使用递归的方式判断子节点的状态
具体做法:
如果一个节点的左右子节点都是平衡的,并且左右子节点的深度差不超过 1,则可以确定这个节点就是一颗平衡二叉树。
public class Solution {
public boolean IsBalanced_Solution (TreeNode pRoot) {
// write code here
//递归
if(pRoot == null){
return true;
}
//判断左右子树是否符合规则,且深度不能超过2
return IsBalanced_Solution(pRoot.left) && IsBalanced_Solution(pRoot.right) && Math.abs(deep(pRoot.left) - deep(pRoot.right))< 2;
}
public int deep(TreeNode root){
if(root == null){
return 0;
}
return Math.max(deep(root.left), deep(root.right)) + 1;
}
}
思路:
我们可以利用二叉搜索树的性质:对于某一个节点若是p与q都小于等于这个这个节点值,说明p、q都在这个节点的左子树,而最近的公共祖先也一定在这个节点的左子树;若是p与q都大于等于这个节点,说明p、q都在这个节点的右子树,而最近的公共祖先也一定在这个节点的右子树。而若是对于某个节点,p与q的值一个大于等于节点值,一个小于等于节点值,说明它们分布在该节点的两边,而这个节点就是最近的公共祖先,因此从上到下的其他祖先都将这个两个节点放到同一子树,只有最近公共祖先会将它们放入不同的子树,每次进入一个子树又回到刚刚的问题,因此可以使用递归。
具体做法:
public class Solution {
public int lowestCommonAncestor (TreeNode root, int p, int q) {
// write code here
//递归
//空树找不到公共祖先
if(root == null){
return -1;
}
//pq在该节点两边说明该节点就是最近公共祖先
if((p >= root.val && q <= root.val) || (p <= root.val && q >= root.val)){
return root.val;
}
//pq都在该节点的左边
else if(p <= root.val && q <= root.val){
//进入左子树
return lowestCommonAncestor(root.left, p , q);
}//pq都在该节点的右侧
else{
//进入右子树
return lowestCommonAncestor(root.right, p, q);
}
}
}
思路:
我们可以讨论几种情况:
public class Solution {
public int lowestCommonAncestor (TreeNode root, int o1, int o2) {
// write code here
//该子树为空,返回-1
if(root == null){
return -1;
}
//该节点是其中某一个节点
if(root.val == o1 || root.val == o2){
return root.val;
}
//左子树寻找公共祖先
int left = lowestCommonAncestor(root.left, o1, o2);
//右子树寻找公共祖先
int right = lowestCommonAncestor(root.right, o1, o2);
//左子树中没找到,则在右子树中
if(left == -1){
return right;
}
//右子树没有找到,则在左子树中
if(right == -1){
return left;
}
//否则是当前节点
return root.val;
}
}
思路:
序列化即将二叉树的节点值取出,放入一个字符串中,我们可以按照前序遍历的思路,遍历二叉树每个节点,并将节点值存储在字符串中,我们用‘#’表示空节点,用‘!'表示节点与节点之间的分割。
反序列化即根据给定的字符串,将二叉树重建,因为字符串中的顺序是前序遍历,因此我们重建的时候也是前序遍历,即可还原。
具体做法:
SerializeFunction(root, res);
//根节点
str.append(root.val).append('!');
//左子树
SerializeFunction(root.left, str);
//右子树
SerializeFunction(root.right, str);
TreeNode res = DeserializeFunction(str);
TreeNode root = new TreeNode(val);
......
//反序列化与序列化一致,都是前序
root.left = DeserializeFunction(str);
root.right = DeserializeFunction(str);
import java.util.*;
public class Solution {
//序列的下标
public int index = 0;
//处理序列化的功能函数(递归)
private void SerializeFunction(TreeNode root, StringBuilder str) {
//如果节点为空,表示左子节点或右子节点为空,用#表示
if (root == null) {
str.append('#');
return;
}
//根节点
str.append(root.val).append('!');
//左子树
SerializeFunction(root.left, str);
//右子树
SerializeFunction(root.right, str);
}
public String Serialize(TreeNode root) {
//处理空树
if (root == null)
return "#";
StringBuilder res = new StringBuilder();
SerializeFunction(root, res);
//把str转换成char
return res.toString();
}
//处理反序列化的功能函数(递归)
private TreeNode DeserializeFunction(String str) {
//到达叶节点时,构建完毕,返回继续构建父节点
//空节点
if (str.charAt(index) == '#') {
index++;
return null;
}
//数字转换
int val = 0;
//遇到分隔符或者结尾
while (str.charAt(index) != '!' && index != str.length()) {
val = val * 10 + ((str.charAt(index)) - '0');
index++;
}
TreeNode root = new TreeNode(val);
//序列到底了,构建完成
if (index == str.length())
return root;
else
index++;
//反序列化与序列化一致,都是前序
root.left = DeserializeFunction(str);
root.right = DeserializeFunction(str);
return root;
}
public TreeNode Deserialize(String str) {
//空序列对应空树
if (str == "#")
return null;
TreeNode res = DeserializeFunction(str);
return res;
}
}
思路:
对于二叉树的前序遍历,我们知道序列的第一个元素必定是根节点的值,因为序列没有重复的元素,因此中序遍历中可以找到相同的这个元素,而我们又知道中序遍历中根节点将二叉树分成了左右子树两个部分
具体做法:
public class Solution {
public TreeNode reConstructBinaryTree (int[] pre, int[] vin) {
// write code here
int n = pre.length;
int m = vin.length;
//每个遍历都不能为0
if (n == 0 || m == 0) {
return null;
}
//构建根节点
TreeNode root = new TreeNode(pre[0]);
for (int i = 0; i < m; i++) {
//找到中序遍历中的前序第一个元素
if (pre[0] == vin[i]) {
//构建左子树
root.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i + 1),
Arrays.copyOfRange(vin, 0, i));
//构建右子树
root.right = reConstructBinaryTree(Arrays.copyOfRange(pre, i + 1, pre.length),
Arrays.copyOfRange(vin, i + 1, vin.length));
break;
}
}
return root;
}
}
思路:
首先呢根据上一题的建树思路,拿到树;然后采用层序遍历的方式 + 辅助队列,最后返回结果
具体做法:
step 1:首先检查树是否为空,为空就不打印。
step 2:建立队列辅助层次遍历,根节点先进队。
step 3:用一个size变量,每次进入一层的时候记录当前队列大小,等到size为0时,便到了最右边,记录下该节点元素。
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 求二叉树的右视图
* @param preOrder int整型一维数组 先序遍历
* @param inOrder int整型一维数组 中序遍历
* @return int整型一维数组
*/
public int[] solve (int[] preOrder, int[] inOrder) {
// write code here
//层次遍历
//先拿到重建的二叉树
TreeNode tree = reConstructBinaryTree(preOrder, inOrder);
if (tree == null) {
return new int[] {};
}
Queue<TreeNode> q = new LinkedList<>();
q.add(tree);
List<Integer> res = new ArrayList<>();
while (!q.isEmpty()) {
int size = q.size();
for (int i = 0; i < size; ++i) {
//取出来de节点
TreeNode cur = q.poll();
//加入到结果集中
//这里是关键,将每一层的最后一个元素加入到res中
if (i == size - 1) {
res.add(cur.val);
}
//看当前节点有没有左右孩子,有的话加入到队列
if (cur.left != null) {
q.offer((cur.left));
}
if (cur.right != null) {
q.offer(cur.right);
}
}
}
//封装右视图
int[] result = new int[res.size()];
for (int i = 0; i < res.size(); i++) {
result[i] = res.get(i);
}
return result;
}
//重建二叉树
public TreeNode reConstructBinaryTree (int[] pre, int[] vin) {
// write code here
int n = pre.length;
int m = vin.length;
//每个遍历都不能为0
if (n == 0 || m == 0) {
return null;
}
//构建根节点
TreeNode root = new TreeNode(pre[0]);
for (int i = 0; i < m; i++) {
//找到中序遍历中的前序第一个元素
if (pre[0] == vin[i]) {
//构建左子树
root.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i + 1),
Arrays.copyOfRange(vin, 0, i));
//构建右子树
root.right = reConstructBinaryTree(Arrays.copyOfRange(pre, i + 1, pre.length),
Arrays.copyOfRange(vin, i + 1, vin.length));
break;
}
}
return root;
}
}
思路:
元素进栈以后,只能优先弹出末尾元素,但是队列每次弹出的却是最先进去的元素,如果能够将栈中元素全部取出来,才能访问到最前面的元素,此时,可以用另一个栈来辅助取出。
具体做法:
//将第一个栈中内容弹出放入第二个栈中
while(!stack1.isEmpty())
stack2.push(stack1.pop());
//再将第二个栈的元素放回第一个栈
while(!stack2.isEmpty())
stack1.push(stack2.pop());
public class Solution {
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
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;
}
}
思路:
我们都知道栈结构的push、pop、top操作都是O(1),但是min函数做不到,于是想到在push的时候就将最小值记录下来,由于栈先进后出的特殊性,我们可以构造一个单调栈,保证栈内元素都是递增的,栈顶元素就是当前最小的元素。
具体做法:
//空或者新元素较小,则入栈
if(s2.isEmpty() || s2.peek() > node)
s2.push(node);
else
//重复加入栈顶
s2.push(s2.peek());
public class Solution {
//用于栈的push和pop
Stack<Integer> s1 = new Stack<>();
//用于存储最小值
Stack<Integer> 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());
}
}
public void pop() {
s1.pop();
s2.pop();
}
public int top() {
return s1.peek();
}
public int min() {
return s2.peek();
}
}
思路:
括号的匹配规则应该符合先进后出原理:最外层的括号即最早出现的左括号,也对应最晚出现的右括号,即先进后出,因此可以使用同样先进后出的栈:遇到左括号就将相应匹配的右括号加入栈中,后续如果是合法的,右括号来的顺序就是栈中弹出的顺序。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param s string字符串
* @return bool布尔型
*/
public boolean isValid (String s) {
// write code here
//辅助栈
Stack<Character> 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('}');
//开始比较,如果栈为空或者栈中弹出的元素与当前元素不相同
}else if(st.isEmpty() || st.pop() != s.charAt(i)){
return false;
}
}
//最终栈为空,返回true;
return st.isEmpty();
}
}
思路:
我们都知道,若是一个数字A进入窗口后,若是比窗口内其他数字都大,那么这个数字之前的数字都没用了,因为它们必定会比A早离开窗口,在A离开之前都争不过A,所以A在进入时依次从尾部排除掉之前的小值再进入,而每次窗口移动要弹出窗口最前面值,因此队首也需要弹出,所以我们选择双向队列。
具体做法:
//先遍历一个窗口
for(int i = 0; i < size; i++){
//去掉比自己先进队列的小于自己的值
while(!dq.isEmpty() && num[dq.peekLast()] < num[i])
dq.pollLast();
dq.add(i);
}
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size) {
ArrayList<Integer> res = new ArrayList<Integer>();
//窗口大于数组长度的时候,返回空
if(size > num.length){
return res;
}
if(size <= num.length && size != 0){
//双向队列
ArrayDeque <Integer> dq = new ArrayDeque<>();
//先遍历一个窗口
for(int i = 0; i < size; i++){
//去掉比自己先进队列的小于自己的值
while(!dq.isEmpty() && num[dq.peekLast()] < num[i])
dq.pollLast();
dq.add(i);
}
//遍历后续数组元素
for(int i = size; i < num.length; i++){
//取窗口内的最大值加入到结果集
res.add(num[dq.peekFirst()]);
while(!dq.isEmpty() && dq.peekFirst() < (i - size + 1))
//弹出窗口移走后的值
dq.pollFirst();
//加入新的值前,去掉比自己先进队列的小于自己的值
while(!dq.isEmpty() && num[dq.peekLast()] < num[i])
dq.pollLast();
dq.add(i);
}
res.add(num[dq.pollFirst()]);
}
return res;
}
}
知识点:优先队列
优先队列即PriorityQueue,是一种内置的机遇堆排序的容器,分为大顶堆与小顶堆,大顶堆的堆顶为最大元素,其余更小的元素在堆下方,小顶堆与其刚好相反。且因为容器内部的次序基于堆排序,因此每次插入元素时间复杂度都是O*(*logn),而每次取出堆顶元素都是直接取出。
思路:
要找到最小的k个元素,只需要准备k个数字,之后每次遇到一个数字能够快速的与这k个数字中最大的值比较,每次将最大的值替换掉,那么最后剩余的就是k个最小的数字了。
如何快速比较k个数字的最大值,并每次替换成较小的新数字呢?我们可以考虑使用优先队列(大根堆),只要限制堆的大小为k,那么堆顶就是k个数字的中最大值,如果需要替换,将这个最大值拿出,加入新的元素就好了。
//较小元素入堆
if(q.peek() > input[i]){
q.poll();
q.offer(input[i]);
}
具体做法:
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution (int[] input, int k) {
// write code here
//大顶堆
ArrayList<Integer> res = new ArrayList<>();
//basecase
if(k == 0 || input.length == 0){
return res;
}
//大根堆
PriorityQueue<Integer> q = new PriorityQueue<>((o1, o2)->o2.compareTo(o1));
//构建一个k个大小的堆
for(int i = 0; i < k; i++){
q.offer(input[i]);
}
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;
}
}
1)在数组范围中,等概率随机
选一个数作为划分值,然后把数组通过荷兰国旗问题分成三个部分:
左侧<划分值、中间==划分值、右侧>划分值
2)对左侧范围和右侧范围,递归执行
3)时间复杂度为O(NlogN)
public class Solution {
public int findKth (int[] a, int n, int K) {
// write code here
return quickSort(a, 0, a.length - 1, K);
}
public int quickSort(int[] nums, int l, int r, int k) {
int index = randomParition(nums, l, r);
if (index == k - 1) {
return nums[index];
} else {
return index > k - 1 ? quickSort(nums, l, index - 1, k) :
quickSort(nums, index + 1, r, k);
}
}
public int randomParition(int[] nums, int l, int r) {
int i = (int) (Math.random() * (r - l)) + l;
swap(nums, i, r);
return partition(nums, l, r);
}
public int partition(int[] nums, int l, int r) {
int pivot = nums[r];
int rightmost = r;
while (l <= r) {
while ( l <= r && nums[l] > pivot ) {
l++;
}
while ( l <= r && nums[r] <= pivot ) {
r--;
}
if (l <= r) {
swap(nums, l, r);
}
}
swap(nums, l, rightmost);
return l;
}
public void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
知识点:插入排序
插入排序是排序中的一种方式,一旦一个无序数组开始排序,它前面部分就是已经排好的有序数组(一开始长度为0),而其后半部分则是需要排序的无序数组,插入排序的做法就是遍历后续需要排序的无序部分,对于每个元素,插入到前半部分有序数组中属于它的位置——即最后一个小于它的元素后。
思路:
传统的寻找中位数的方法便是排序之后,取中间值或者中间两位的平均即可。但是这道题因为数组在不断增长,每增长一位便需要排一次,很浪费时间,于是可以考虑在增加数据的同时将其有序化,这个过程就让我们想到了插入排序:对于每个输入的元素,遍历已经有序的数组,将其插入到属于它的位置。
int i = 0;
//遍历找到插入点
for(; i < val.size(); i++){
if(num <= val.get(i))
break;
}
//插入相应位置
val.add(i, num);
具体做法:
public class Solution {
//new一个集合存储输入的数据流
ArrayList<Integer> arr = new ArrayList<>();
public void Insert(Integer num) {
//arr中没有数据,直接加入
if(arr.isEmpty()){
arr.add(num);
}
//arr中有数据,需要插入排序
else{
int i = 0;
//遍历找到插入点
for(; i < arr.size(); ++i){
if(num < arr.get(i))
break;
}
//插入相应的位置
arr.add(i, num);
}
}
public Double GetMedian() {
int n = arr.size();
//奇数个数字
if(n % 2 == 1){
//类型转换
return (double)arr.get(n / 2);
}else{
//偶数个数字
double a = arr.get(n / 2);
double b = arr.get(n / 2 - 1);
return (a + b) / 2;
}
}
}
【双栈思路】
情况1:是数,直接压nums栈;
情况2:是 ‘(’ ,直接压opts栈;
情况3:是 ‘)’ ,先计算opts栈中 ‘(’ 前的操作符,然后将 '('弹出;
情况4:是 ‘±*’ ,先计算opts栈中 ‘(’ 前的、优先级>=它的操作符,然后将它压栈;
import java.util.*;
public class Solution {
public static int solve (String s) {
Map<Character,Integer> map = new HashMap<>(); //存优先级的map
map.put('-', 1);
map.put('+', 1);
map.put('*', 2);
Deque<Integer> nums = new ArrayDeque<>(); // 数字栈
Deque<Character> opts = new ArrayDeque<>(); // 操作符栈
s.replaceAll(" ",""); // 去空格
char[] chs = s.toCharArray();
int n = chs.length;
for(int i = 0; i < n; i++){
char c = chs[i];
if(isNumber(c)){ // 情况1
int num = 0;
int j = i;
// 读取连续数字符号
while(j < n && isNumber(chs[j])){
num = 10*num + chs[j++] - '0';
}
nums.push(num);
i = j - 1;
}else if (c == '('){ // 情况2
opts.push(c);
}else if (c == ')' ){ // 情况3
while(opts.peek() != '('){
cal(nums, opts);
}
opts.pop();
}else{ // 情况4
while(!opts.isEmpty() && opts.peek()!='(' && map.get(opts.peek()) >= map.get(c)){
cal(nums, opts);
}
opts.push(c);
}
}
while(!opts.isEmpty()) { // 计算栈中剩余操作符
cal(nums, opts);
}
return nums.pop();
}
public static boolean isNumber(Character c){
return Character.isDigit(c);
}
public static void cal(Deque<Integer> nums, Deque<Character> opts){
int num2 = nums.pop();
int num1 = nums.pop();
char opt = opts.pop();
if(opt == '+'){
nums.push(num1 + num2);
}else if(opt == '-'){
nums.push(num1 - num2);
}else if(opt == '*'){
nums.push(num1 * num2);
}
}
}
思路:
我们能想到最直观的解法,可能就是两层遍历,将数组所有的二元组合枚举一遍,看看是否是和为目标值,但是这样太费时间了,既然加法这么复杂,我们是不是可以尝试一下减法:对于数组中出现的一个数a,如果目标值减去a的值已经出现过了,那这不就是我们要找的一对元组吗?这种时候,快速找到已经出现过的某个值,可以考虑使用哈希表快速检验某个元素是否出现过这一功能。
public class Solution {
public int[] twoSum (int[] numbers, int target) {
int[] res = new int[0];
//创建哈希表,两元组分别表示值、下标
HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
//在哈希表中查找target-numbers[i]
for (int i = 0; i < numbers.length; i++) {
int differ = target - numbers[i];
//若是没找到,将此信息计入哈希表
if (!map.containsKey(differ)) {
map.put(numbers[i], i);
}
//否则返回两个下标+1
else{
return new int[] {map.get(differ) + 1, i + 1};
}
}
return res;
}
}
思路:
首先我们分析一下,数组某个元素出现次数超过了数组长度的一半,那它肯定出现最多,而且只要超过了一半,其他数字不可能超过一半了,必定是它。
如果给定的数组是有序的,那我们在连续的相同数字中找到出现次数最多即可,但是题目没有要求有序,一种方法是对数组排序后解决,但是时间复杂度就上去了。那我们可以考虑遍历一次数组统计各个元素出现的次数,找到出现次数大于数组长度一半的那个数字。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param numbers int整型一维数组
* @return int整型
*/
public int MoreThanHalfNum_Solution (int[] nums) {
//哈希表统计每个数字出现的次数
HashMap<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
if (!map.containsKey(num)) {
map.put(num, 0);
}
map.put(num, map.get(num) + 1);
}
int half = nums.length / 2;
for (int key : map.keySet()) {
if (map.get(key) > half) {
return key;
}
}
return -1;
}
}
知识点:哈希表
哈希表是一种根据关键码(key)直接访问值(value)的一种数据结构。而这种直接访问意味着只要知道key就能在O(1)时间内得到value,因此哈希表常用来统计频率、快速检验某个元素是否出现过等。
思路:
既然有两个数字只出现了一次,我们就统计每个数字的出现次数,利用哈希表的快速根据key值访问其频率值。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param nums int整型一维数组
* @return int整型一维数组
*/
public int[] FindNumsAppearOnce (int[] nums) {
// write code here
HashMap<Integer,Integer> map = new HashMap<>();
ArrayList<Integer> res = new ArrayList<>();
//遍历数组
for(int i = 0; i < nums.length; ++i){
//统计每个数出现的频率
if(!map.containsKey(nums[i])){
map.put(nums[i], 1);
}else{
map.put(nums[i], map.get(nums[i]) + 1);
}
}
//再次遍历数组
for(int i = 0; i < nums.length; i++){
//找到频率为1的两个数
if(map.get(nums[i]) == 1){
res.add(nums[i]);
}
}
//整理次序
if(res.get(0) < res.get(1)){
return new int[]{res.get(0),res.get(1)};
}else{
return new int[]{res.get(1),res.get(0)};
}
}
}
思路:
n个长度的数组,没有重复,则如果数组填满了1~n,那么缺失n+1,如果数组填不满1~n,那么缺失的就是1~n中的数字。对于这种快速查询某个元素是否出现过的问题,还是可以使用哈希表快速判断某个数字是否出现过。
具体做法:
public class Solution {
public int minNumberDisappeared (int[] nums) {
// write code here
//map用来存数组中的元素
HashMap<Integer, Integer> map = new HashMap<>();
//将数组中的元素丢进map中
for (int i = 0; i < nums.length; ++i) {
map.put(nums[i], 1);
}
int res = 1;
//从1开始找到哈希表中第一个没有出现的正整数
while (map.containsKey(res)){
res++;
}
return res;
}
}
知识点1:哈希表
哈希表是一种根据关键码(key)直接访问值(value)的一种数据结构。而这种直接访问意味着只要知道key就能在O(1)时间内得到value,因此哈希表常用来统计频率、快速检验某个元素是否出现过等。
知识点2:双指针
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
思路:
直接找三个数字之和为某个数,太麻烦了,我们是不是可以拆分一下:如果找到了某个数a*,要找到与之对应的另外两个数,三数之和为0,那岂不是只要找到另外两个数之和为−*a?这就方便很多了。
因为三元组内部必须是有序的,因此可以优先对原数组排序,这样每次取到一个最小的数为a,只需要在后续数组中找到两个之和为−a就可以了,我们可以用双指针缩小区间,因为太后面的数字太大了,就不可能为−a,可以舍弃。
具体做法:
注:对于三个数字都要判断是否相邻有重复的情况,要去重。
import java.util.*;
class Solution {
public ArrayList<ArrayList<Integer>> threeSum(int[] nums) {
ArrayList<ArrayList<Integer> > res = new ArrayList<ArrayList<Integer>>();
Arrays.sort(nums);
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.length; i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return res;
}
if (i > 0 && nums[i] == nums[i - 1]) { // 去重a
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (right > left) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
} else if (sum < 0) {
left++;
} else {
ArrayList<Integer> temp = new ArrayList<Integer>();
temp.add(nums[i]);
temp.add(nums[left]);
temp.add(nums[right]);
res.add(temp);
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
return res;
}
}
思路:
public class Solution {
public ArrayList<ArrayList<Integer>> permute (int[] nums) {
/**
回溯法
*/
//首先需要一个二维数组List来存放结果
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
//然后需要一个HashMap来判断是否该元素被使用过
HashMap<Integer, Boolean> visited = new HashMap<>();
//将元素丢进map里,value初始为false
for (int num : nums) {
visited.put(num, false);
}
//回溯法
backtracking(nums, res, visited, new ArrayList<>());
//返回结果
return res;
}
//重头戏回溯法 list用来存放当前递归路径上的组合
void backtracking(int[] nums, ArrayList<ArrayList<Integer>> result,
HashMap<Integer, Boolean> visited, ArrayList<Integer> list) {
//剪枝条件
if (list.size() == nums.length) {
//这里直接复制,避免引用传递
result.add(new ArrayList<>(list));
return;
}
//如果没有从递归里面退出去
for (int i = 0; i < nums.length; ++i) {
int num = nums[i];
//map里面的num值出现的情况下直接进入下一层循环,就不要往下走了
//map里面的num值没有出现的情况下
if (!visited.get(num)) {
list.add(num);
visited.put(num, true);
backtracking(nums, result, visited, list);
//将list的最后一个数删掉
list.remove(list.size() - 1);
//将num值在进入下一层递归之前改为false
visited.put(num, false);
}
}
}
}
思路:
这道题类似没有重复项数字的全排列,但是因为交换位置可能会出现相同数字交换的情况,出现的结果需要去重,因此不便于使用交换位置的方法。
我们就使用临时数组去组装一个排列的情况:每当我们选取一个数组元素以后,就确定了其位置,相当于对数组中剩下的元素进行全排列添加在该元素后面,给剩余部分进行全排列就是一个子问题,因此可以使用递归。
回溯的思想也与没有重复项数字的全排列类似,对于数组[1,2,2,3],如果事先在临时数组中加入了1,后续子问题只能是[2,2,3]的全排列接在1后面,对于2开头的分支达不到,因此也需要回溯:将临时数组刚刚加入的数字pop掉,同时vis修改为没有加入,这样才能正常进入别的分支。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param num int整型一维数组
* @return int整型ArrayList>
*/
public ArrayList<ArrayList<Integer>> permuteUnique (int[] num) {
// write code here
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
//basecase
if (num == null || num.length == 0) {
return res;
}
//先排序
Arrays.sort(num);
Boolean[] vis = new Boolean[num.length];
//将布尔数组 vis 中的所有元素都设置为 false
Arrays.fill(vis, false);
// temp用来存放当前递归路径上的组合
ArrayList<Integer> temp = new ArrayList<>();
//递归
recursion(res, num, temp, vis);
return res;
}
public void recursion(ArrayList<ArrayList<Integer>> res, int[] num,
ArrayList<Integer> temp, Boolean[] vis) {
//临时数组满了就加入到res,剪枝条件
if (temp.size() == num.length) {
res.add(new ArrayList<Integer>(temp));
return;
}
//遍历所有元素选取一个加入
for (int i = 0; i < num.length; i++) {
//如果该元素以及被加入了就不用加入了
if (vis[i]) {
continue;
}
//当前的元素num[i]与同一层的前一个元素num[i-1]相同且num[i-1]已经用过了
if (i > 0 && num[i] == num[i - 1] && !vis[i - 1]) {
continue;
}
//标记为使用过
vis[i] = true;
//加入数组
temp.add(num[i]);
//递归
recursion(res, num, temp, vis);
//将temp的最后一个数删掉
temp.remove(temp.size() - 1);
//在进入下一层递归之前改为false
vis[i] = false;
}
}
}
知识点:深度优先搜索(dfs) 深度优先搜索一般用于树或者图的遍历,其他有分支的(如二维矩阵)也适用。它的原理是从初始点开始,一直沿着同一个分支遍历,直到该分支结束,然后回溯到上一级继续沿着一个分支走到底,如此往复,直到所有的节点都有被访问到。
思路:
矩阵中多处聚集着1,要想统计1聚集的堆数而不重复统计,那我们可以考虑每次找到一堆相邻的1,就将其全部改成0,而将所有相邻的1改成0的步骤又可以使用深度优先搜索(dfs):当我们遇到矩阵的某个元素为1时,首先将其置为了0,然后查看与它相邻的上下左右四个方向,如果这四个方向任意相邻元素为1,则进入该元素,进入该元素之后我们发现又回到了刚刚的子问题,又是把这一片相邻区域的1全部置为0,因此可以用递归实现。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 判断岛屿数量
* @param grid char字符型二维数组
* @return int整型
*/
public int solve (char[][] grid) {
/**
DFS
*/
if (grid == null || grid.length == 0) {
return 0;
}
int result = 0;
int row = grid.length;
int col = grid[0].length;
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if (grid[i][j] == '1') {
result++;
dfs(grid, i, j, row, col);
}
}
}
return result;
}
private void dfs(char[][] grid, int x, int y, int row, int col) {
if (x < 0 || y < 0 || x >= row || y >= col || grid[x][y] == '0') {
return;
}
grid[x][y] = '0';
dfs(grid, x - 1, y, row, col);
dfs(grid, x + 1, y, row, col);
dfs(grid, x, y - 1, row, col);
dfs(grid, x, y + 1, row, col);
}
}
知识点:递归与回溯
递归是一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。因此递归过程,最重要的就是查看能不能讲原本的问题分解为更小的子问题,这是使用递归的关键。
如果是线型递归,子问题直接回到父问题不需要回溯,但是如果是树型递归,父问题有很多分支,我需要从子问题回到父问题,进入另一个子问题。因此回溯是指在递归过程中,从某一分支的子问题回到父问题进入父问题的另一子问题分支,因为有时候进入第一个子问题的时候修改过一些变量,因此回溯的时候会要求改回父问题时的样子才能进入第二子问题分支。
思路:
都是求元素的全排列,字符串与数组没有区别,一个是数字全排列,一个是字符全排列,因此大致思路与有重复项数字的全排列类似,只是这道题输出顺序没有要求。但是为了便于去掉重复情况,我们还是应该参照数组全排列,优先按照字典序排序,因为排序后重复的字符就会相邻,后续递归找起来也很方便。
使用临时变量去组装一个排列的情况:每当我们选取一个字符以后,就确定了其位置,相当于对字符串中剩下的元素进行全排列添加在该元素后面,给剩余部分进行全排列就是一个子问题,因此可以使用递归。
递归过程也需要回溯,比如说对于字符串“abbc”
,如果事先在临时字符串中加入了a,后续子问题只能是"bbc"
的全排列接在a后面,对于b开头的分支达不到,因此也需要回溯:将临时字符串刚刚加入的字符去掉,同时vis修改为没有加入,这样才能正常进入别的分支。
具体做法:
public class Solution {
public ArrayList<String> Permutation (String str) {
// write code here
ArrayList<String> res = new ArrayList<>();
//basecase
if (str == null || str.length() == 0) {
return res;
}
//将字符串转字符数组
char[] charStr = str.toCharArray();
//排序
Arrays.sort(charStr);
//布尔数组中的每个元素用于标记字符串中对应位置的字符是否被访问过或处理过
boolean[] vis = new boolean[str.length()];
//标记每个位置的字符是否被使用过,默认false
//Arrays.fill(vis, false) 是一个数组工具类 Arrays 的静态方法,
//用于将布尔数组 vis 中的所有元素都设置为 false。
Arrays.fill(vis, false);
StringBuffer temp = new StringBuffer();
//递归获取
recursion(charStr, res, temp, vis);
return res;
}
public void recursion(char[] str, ArrayList<String> res, StringBuffer temp,
boolean[] vis) {
// 剪枝条件,当temp满了就加入res
if (temp.length() == str.length) {
res.add(new String(temp));
return;
}
//遍历所有元素选取一个加入
for (int i = 0; i < str.length; i++) {
//如果该元素已经被加入了就不用在加入了
if (vis[i]) {
continue;
}
//如果当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经用过了
if (i > 0 && str[i - 1] == str[i] && !vis[i - 1]) {
//跳出本次循环
continue;
}
//标记为未使用
vis[i] = true;
//加入到临时字符串
temp.append(str[i]);
//回溯
recursion(str, res, temp, vis);
//在进入下一层递归之前改为false
vis[i] = false;
//将temp的最后一个字符删掉
temp.deleteCharAt(temp.length() - 1);
}
}
}
n个皇后,不同行不同列,那么肯定棋盘每行都会有一个皇后,每列都会有一个皇后。如果我们确定了第一个皇后的行号与列号,则相当于接下来在n−1行中查找n−1个皇后,这就是一个子问题,因此使用递归:
具体做法:
public class Solution {
private int res;
public int Nqueen (int n) {
// write code here
res = 0;
//下标为行号,元素为列号,记录皇后的位置
int[] pos = new int[n];
//
Arrays.fill(pos, 0);
//递归
recursion(n, 0, pos);
return res;
}
//递归查找皇后的种类
private void recursion(int n, int row, int[] pos) {
// 当最后一行都被选择了位置,说明n个皇后位置齐了,增加一种方案数返回
if (row == n) {
res++;
return;
}
//遍历所有的列
for (int i = 0; i < n; i++) {
//检查该位置是否符合条件
if (isValid(pos, row, i)) {
//加入位置
pos[row] = i;
//递归继续查找
recursion(n, row + 1, pos);
}
}
}
//判断皇后是否符合条件
public boolean isValid(int[] pos, int row, int col) {
//遍历所有已经记录的行
for (int i = 0; i < row; i++) {
//不能同行同列同一斜线
if (row == i || col == pos[i] || Math.abs(row - i) == Math.abs(col - pos[i]))
return false;
}
return true;
}
}
思路:
相当于一共n个左括号和n个右括号,可以给我们使用,我们需要依次组装这些括号。每当我们使用一个左括号之后,就剩下n−1个左括号和n个右括号给我们使用,结果拼在使用的左括号之后就行了,因此后者就是一个子问题,可以使用递归:
但是这样递归不能保证括号一定合法,我们需要保证左括号出现的次数比右括号多时我们再使用右括号就一定能保证括号合法了,因此每次需要检查左括号和右括号的使用次数。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param n int整型
* @return string字符串ArrayList
*/
public ArrayList<String> generateParenthesis (int n) {
/**
* 回溯法
l:左边括号的数量
r:右边括号的数量
str: 括号的样式
*/
ArrayList<String> res = new ArrayList<>();
//递归
backtracking(n, res, 0, 0, "");
return res;
}
void backtracking(int n, List res, int l, int r, String str) {
//r 的数量一旦大于l,终止
if (l < r) {
return;
}
//左右括号都用完了,就加入结果
if (l == n && r == n) {
res.add(str);
return;
}
//使用一次左括号
if (l < n) {
backtracking(n, res, l + 1, r, str + "(");
}
//使用右括号个数必须少于左括号
if (r < l) {
backtracking(n, res, l, r + 1, str + ")");
}
}
}
思路:
既然是查找最长的递增路径长度,那我们首先要找到这个路径的起点,起点不好直接找到,就从上到下从左到右遍历矩阵的每个元素。然后以每个元素都可以作为起点查找它能到达的最长递增路径。
如何查找以某个点为起点的最长递增路径呢?我们可以考虑深度优先搜索,因为我们查找递增路径的时候,每次选中路径一个点,然后找到与该点相邻的递增位置,相当于进入这个相邻的点,继续查找递增路径,这就是递归的子问题。因此递归过程如下:
具体做法:
public class Solution {
int maxStep = 0;
public int solve (int[][] matrix) {
// write code here
//因为不知道起始点,所以所有的点都要试一遍
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[0].length; j++) {
dfs(1, i, j, matrix, -1);
}
}
return maxStep ;
}
private void dfs(int steps, int i, int j, int[][] matrix, int last) {
//边界判定,以及小于上一个值的也属于走不通的
if (i < 0 || i >= matrix.length
|| j < 0 || j >= matrix.length
|| matrix[i][j] <= last) {
return;
}
//记录备份当前值
int cur = matrix[i][j];
//将走过的路涂黑,这里就是变成-1
matrix[i][j] = -1;
maxStep = Math.max(steps, maxStep );
//向下
dfs(steps + 1, i + 1, j, matrix, cur);
//向上
dfs(steps + 1, i - 1, j, matrix, cur);
//向右
dfs(steps + 1, i, j + 1, matrix, cur);
//向左
dfs(steps + 1, i, j - 1, matrix, cur);
//走完上面以后说明要回退了,重新给这个点赋值回去
matrix[i][j] = cur;
}
}
知识点:动态规划
动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。
思路:
斐波那契数列初始化第1项与第2项都是1,则根据公式第0项为0,可以按照斐波那契公式累加到第n项。
具体做法:
public class Solution {
public int Fibonacci (int n) {
// write code here
//从 0 开始,第零项是0,第一项是1
//低于2项的数列,直接返回n。
if(n <= 1){
return n;
}
//初始化第0项,与第1项分别为0,1.
//因n=2时也为1,初始化的时候a = 0,b = 1
int res = 0;
int a = 0;
int b = 1;
for(int i = 2; i <= n; i++){
//第三项开始是前两项的和,然后保留最新的两项,更新数据相加
res = (a + b);
a = b;
b = res;
}
return res;
}
}
思路:
一只青蛙一次可以跳1阶或2阶,直到跳到第n阶,也可以看成这只青蛙从n阶往下跳,到0阶,按照原路返回的话,两种方法事实上可以的跳法是一样的——即怎么来的,怎么回去! 当青蛙在第n阶往下跳,它可以选择跳1阶到n−1,也可以选择跳2阶到 n−2,即它后续的跳法变成了f(n−1)+f(n−2),这就变成了斐波那契数列。因此可以按照斐波那契数列的做法来做:即输入n,输出第n个斐波那契数列的值,初始化0阶有1种,1阶有1种。
具体做法:
public class Solution {
public int jumpFloor(int target) {
//从0开始,第0项是0,第一项是1
if(target <= 1)
return 1;
int res = 0;
int a = 0;
int b = 1;
//因n=2时也为1,初始化的时候把a=0,b=1
for(int i = 2; i <= target + 1; i++){
//第三项开始是前两项的和,然后保留最新的两项,更新数据相加
res = (a + b);
a = b;
b = res;
}
return res ;
}
思路:
题目同样考察斐波那契数列的动态规划实现,不同的是题目要求了最小的花费,因此我们将方案统计进行递推的时候只记录最小的开销方案即可。
具体做法:
dp[i]=min(dp[i−1]+cost[i−1],dp[i−2]+cost[i−2])
。public class Solution {
public int minCostClimbingStairs (int[] cost) {
//dp[i]表示爬到第i阶楼梯需要的最小花费
int[] dp = new int[cost.length + 1];
for (int i = 2; i <= cost.length; i++)
//每次选取最小的方案
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
return dp[cost.length];
}
}
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* longest common subsequence
* @param s1 string字符串 the string
* @param s2 string字符串 the string
* @return string字符串
*/
public String LCS (String s1, String s2) {
// write code here
//1. 确定dp数组(dp table)以及下标的含义
//2. 确定递推公式
//3. dp数组如何初始化
//4. 确定遍历顺序
//5. 举例推导dp数组
int n = s1.length();
int m = s2.length();
//创建一个二维数组 dp,用于存储中间状态的计算结果。
//dp[i][j] 表示 s1 前 i 个字符和 s2 前 j 个字符的最长公共子序列的长度
int[][] dp = new int[n + 1][m + 1];
for (int i = 1; i <= n ; i++) {
for (int j = 1; j <= m; j++) {
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
if (dp[n][m] == 0 ) {
return "-1";
}
StringBuilder sb = new StringBuilder();
while (n > 0 && m > 0) {
if (s1.charAt(n - 1) == s2.charAt(m - 1)) {
sb.append(s1.charAt(n - 1));
n--;
m--;
} else {
if (dp[n - 1][m] > dp[n][m - 1]) {
n--;
} else {
m--;
}
}
}
return sb.reverse().toString();
}
}
具体做法:
dp[i][j]
表示在str1中以第i个字符结尾,在str2中以第j个字符结尾时的公共子串长度,dp[i][j]=dp[i−1][j−1]+1
,如果遍历到该位时两个字符不相等,则置为0,因为这是子串,必须连续相等,断开要重新开始。dp[i][j]
后,我们维护最大值,并更新该子串结束位置。public class Solution {
public String LCS (String str1, String str2) {
//dp[i][j]表示到str1第i个到str2第j个为止的公共子串长度
int n = str1.length();
int m = str2.length();
int[][] dp = new int[n + 1][m + 1];
int max = 0;
int pos = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
//如果该两位相同
if(str1.charAt(i - 1) == str2.charAt(j - 1))
//则增加长度
dp[i][j] = dp[i - 1][j - 1] + 1;
else
//该位置为0
dp[i][j] = 0;
//更新最大长度
if(dp[i][j] > max){
max = dp[i][j];
pos = i - 1;
}
}
}
return str1.substring(pos - max + 1, pos + 1);
}
}
思路:
如果我们此时就在右下角的格子,那么能够到达该格子的路径只能是它的上方和它的左方两个格子,因此从左上角到右下角的路径数应该是从左上角到它的左边格子和上边格子的路径数之和,因此可以动态规划。
具体做法:
dp[i][j]
表示大小为i∗j
的矩阵的路径数量,下标从1开始。dp[i][j]=dp[i−1][j]+dp[i][j−1]
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param m int整型
* @param n int整型
* @return int整型
*/
public int uniquePaths (int m, int n) {
// write code here
int[][] dp = new int[m + 1][n + 1];
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
//只有一行的时候,只有一种路径
if(i == 1){
dp[i][j] = 1;
continue;
}
//只有一列的时候,只有一种路径
if(j == 1){
dp[i][j] = 1;
continue;
}
//路径的长度等于左边的格子加上上方的格子
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m][n];
}
}
这题求的是从左上角到右下角,路径上的数字和最小,并且每次只能向下或向右移动。所以上面很容易想到动态规划求解。我们可以使用一个二维数组dp
,dp[i][j]
表示的是从左上角到坐标(i,j)
的最小路径和。那么走到坐标(i,j)
的位置只有这两种可能,要么从上面(i-1,j)
走下来,要么从左边(i,j-1)
走过来,我们要选择路径和最小的再加上当前坐标的值就是到坐标(i,j)
的最小路径。
所以递推公式就是
dp[i][j]=min(dp[i-1][j]+dp[i][j-1])+grid[i][j];
有了递推公式再来看一下边界条件,当在第一行的时候,因为不能从上面走下来,所以当前值就是前面的累加。同理第一列也一样,因为他不能从左边走过来,所以当前值只能是上面的累加。
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param matrix int整型二维数组 the matrix
* @return int整型
*/
public int minPathSum (int[][] matrix) {
// write code here
int m = matrix.length;
int n = matrix[0].length;
int[][] dp = new int[m][n];
dp[0][0] = matrix[0][0];
//第一列只能从上面走下来
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + matrix[i][0];
}
//第一行只能从左边走过来
for (int i = 1; i < n; i++) {
dp[0][i] = dp[0][i - 1] + matrix[0][i];
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
//递推公式,取从上面走下来和从左边走过来的最小值+当前坐标的值
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + matrix[i][j];
}
}
return dp[m - 1][n - 1];
}
}
我们看到二维数组dp
和二维数组matrix
的长和宽都是一样的,没必要再申请一个dp
数组,完全可以使用matrix
,来看下代码
public int minPathSum(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 && j == 0)
continue;
if (i == 0) {
//第一行只能从左边走过来
matrix[i][j] += matrix[i][j - 1];
} else if (j == 0) {
//第一列只能从上面走下来
matrix[i][j] += matrix[i - 1][j];
} else {
//递推公式,取从上面走下来和从左边走过来的最小值+当前坐标的值
matrix[i][j] += Math.min(matrix[i - 1][j], matrix[i][j - 1]);
}
}
}
return matrix[m - 1][n - 1];
}
思路:
对于普通数组1-9,译码方式只有一种,但是对于11-19,21-26,译码方式有可选择的两种方案,因此我们使用动态规划将两种方案累计。
具体做法:
dp[i]=dp[i−1]
;如果组合译码,则dp[i]=dp[i−2]
。dp[i−1]
,对于满足两种译码方式(10,20不能)则是dp[i−1]+dp[i−2]
dp[length]
即为所求答案。public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 解码
* @param nums string字符串 数字串
* @return int整型
*/
public int solve (String nums) {
// write code here
//排除 0
if (nums.equals("0")) {
return 0;
}
//排除只有一种可能的10 20
if (nums == "10" || nums == "20" ) {
return 1;
}
//当0 的前面不是1 或者2时,无法编译,0 种
for (int i = 1; i < nums.length(); i++) {
if (nums.charAt(i) == '0') {
if (nums.charAt(i - 1) != '1' && nums.charAt(i - 1) != '2') {
return 0;
}
}
}
int[] dp = new int[nums.length() + 1];
//辅助数组初始化为1
Arrays.fill(dp, 1);
for (int i = 2; i <= nums.length(); i++) {
//在11-19,21-26之间的情况
if ((nums.charAt(i - 2) == '1' && nums.charAt(i - 1) != '0') ||
(nums.charAt(i - 2) == '2' && nums.charAt(i - 1) > '0' &&
nums.charAt(i - 1) < '7')) {
dp[i] = dp[i - 1] + dp[i - 2];
}
else {
dp[i] = dp[i - 1];
}
}
return dp[nums.length()];
}
}
思路:
这类涉及状态转移的题目,可以考虑动态规划。
具体做法:
dp[i]
表示要凑出i元钱需要最小的货币数。aim+1
,因此货币最小1元,即货币数不会超过aim
.dp[0]=0
。dp[i]=min(dp[i],dp[i−arr[j]]+1)
.dp[aim]
的值是否超过aim,如果超过说明无解,否则返回即可。public int minMoney (int[] arr, int aim) {
// write code here
//小于1的都返回0
if (aim < 1) {
return 0;
}
int[] dp = new int[aim + 1];
//dp[i] 表示凑齐i元最少需要多少货币数
Arrays.fill(dp, aim + 1);
dp[0] = 0;
//遍历1- aim元
for (int i = 1; i <= aim; i++) {
//每种面值的货币都要枚举
for (int j = 0; j < arr.length; j++) {
//如果面值不超过要凑的钱才能用
if (arr[j] <= i)
//维护最小值
dp[i] = Math.min(dp[i], dp[i - arr[j]] + 1);
}
}
//如果最终答案大于aim代表无解
return dp[aim] > aim ? -1 : dp[aim];
}
思路:
要找到最长的递增子序列长度,每当我们找到一个位置,它是继续递增的子序列还是不是,它选择前面哪一处接着才能达到最长的递增子序列,这类有状态转移的问题常用方法是动态规划。
具体做法:
dp[i]
表示到元素i结尾时,最长的子序列的长度,初始化为1,因为只有数组有元素,至少有一个算是递增。dp[i]=dp[j]+1
。public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 给定数组的最长严格上升子序列的长度。
* @param arr int整型一维数组 给定的数组
* @return int整型
*/
public int LIS (int[] arr) {
// write code here
if(arr.length == 1){
return 1;
}
int dp[] = new int[arr.length];
//设置数组长度大小的动态规划辅助数组
Arrays.fill(dp, 1);
int res = 0;
for(int i = 1; i < arr.length; i++){
for(int j = 0; j < i; j++){
//可能j不是所需要的最大的,因此需要dp[i] = dp[j] + 1
if(arr[i] > arr[j] && dp[i] < dp[j] + 1){
//i 点比 j 点大,理论上dp要加1
dp[i] = dp[j] + 1;
//找到最大长度
res = Math.max(res, dp[i]);
}
}
}
return res;
}
}
解题思路
方法1:连续的子数组,即数组中从i下标到j下标(0<=i<=j<数组长度)的数据,想要获得所有的子数组和,可以通过暴力法,两次循环获得,但时间复杂度为O(n^2),效率不高。
方法2:动态规划,设动态规划列表 dp,dp[i] 代表以元素 array[i] 为结尾的连续子数组最大和。
状态转移方程: dp[i] = Math.max(dp[i-1]+array[i], array[i])
;
具体思路如下:
方法3:我们可以简化动态规划,使用一个变量sum来表示当前连续的子数组和,以及一个变量max来表示中间出现的最大的和。
代码实现
方法1:暴力法,时间复杂度O(n^2),空间复杂度O(1)
public int FindGreatestSumOfSubArray(int[] array) {
int max = array[0];
int sum = 0;
for(int i=0;i<array.length;i++){
// 每开启新的循环,需要把sum归零
sum = 0;
for(int j=i;j<array.length;j++){
// 这里是求从i到j的数值和
sum += array[j];
// 每次比较,保存出现的最大值
max = Math.max(max,sum);
}
}
return max;
}
方法2:动态规划,时间复杂度O(n),空间复杂度O(n)
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param array int整型一维数组
* @return int整型
*/
public int FindGreatestSumOfSubArray (int[] array) {
// write code here
int[] dp = new int[array.length];
dp[0] = array[0];
int max = array[0];
for (int i = 1; i < array.length; i++) {
//状态转移方程
dp[i] = Math.max(dp[i - 1] + array[i], array[i]);
//更新最大值
max = Math.max(max, dp[i]);
}
return max;
}
}
方法3:优化动态规划,时间复杂度O(n),空间复杂度O(1)
public int FindGreatestSumOfSubArray(int[] array) {
int sum = 0;
int max = array[0];
for(int i=0;i<array.length;i++){
// 优化动态规划,确定sum的最大值
sum = Math.max(sum + array[i], array[i]);
// 每次比较,保存出现的最大值
max = Math.max(max,sum);
}
return max;
}
1、中心扩散法
中心扩散法也很好理解,我们遍历字符串的每一个字符,然后以当前字符为中心往两边扩散,查找最长的回文子串
但是回文串的长度不一定都是奇数,也可能是偶数,比如字符串"abba"。我们来思考这样一个问题,如果是单个字符,我们可以认为他是回文子串,如果是多个字符,并且他们都是相同的,那么他们也是回文串。
所以对于上面的问题,我们以当前字符为中心往两边扩散的时候,先要判断和他挨着的有没有相同的字符,如果有,则直接跳过,来看下代码
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param A string字符串
* @return int整型
*/
public int getLongestPalindrome (String A, int n) {
// write code here
if (n < 2) {
return A.length();
}
//maxLen 表示最长回文串的长度
int maxLen = 0;
for (int i = 0; i < n; ) {
//如果剩余子串长度小于目前查找到的最长回文子串的长度,直接终止循环
// (因为即使他是回文子串,也不是最长的,所以直接终止循环,不再判断)
if (n - i <= maxLen / 2)
break;
int left = i;
int right = i;
while (right < n - 1 && A.charAt(right + 1) == A.charAt(right))
++right; //过滤掉重复的
//下次在判断的时候从重复的下一个字符开始判断
i = right + 1;
//然后往两边判断,找出回文子串的长度
while (right < n - 1 && left > 0
&& A.charAt(right + 1) == A.charAt(left - 1)) {
++right;
--left;
}
//保留最长的
if (right - left + 1 > maxLen) {
maxLen = right - left + 1;
}
}
//截取回文子串
return maxLen;
}
}
2、暴力求解
暴力求解是最容易想到的,要截取字符串的所有子串,然后再判断这些子串中哪些是回文的,最后返回回文子串中最长的即可。
这里我们可以使用两个变量,一个记录最长回文子串开始的位置,一个记录最长回文子串的长度,最后再截取。代码如下
public int getLongestPalindrome(String A, int n) {
//边界条件判断
if (n < 2)
return A.length();
int maxLen = 0;
for (int i = 0; i < n - 1; i++) {
for (int j = i; j < n; j++) {
//截取所有子串,然后在逐个判断是否是回文的
if (isPalindrome(A, i, j)) {
if (maxLen < j - i + 1) {
maxLen = j - i + 1;
}
}
}
}
return maxLen;
}
//判断是否是回文串
private boolean isPalindrome(String s, int start, int end) {
while (start < end) {
if (s.charAt(start++) != s.charAt(end--))
return false;
}
return true;
}
实际上面代码其实还可以优化一下,在截取的时候,如果截取的长度小于等于目前查找到的最长回文子串,我们可以直接跳过,不需要再判断了,因为即使他是回文子串,也不可能是最长的
public int getLongestPalindrome(String A, int n) {
//边界条件判断
if (n < 2)
return A.length();
int maxLen = 0;
for (int i = 0; i < n - 1; i++) {
for (int j = i; j < n; j++) {
//截取所有子串,然后在逐个判断是否是回文的
if (isPalindrome(A, i, j)) {
if (maxLen < j - i + 1) {
maxLen = j - i + 1;
}
}
}
}
return maxLen;
}
//判断是否是回文串
private boolean isPalindrome(String s, int start, int end) {
while (start < end) {
if (s.charAt(start++) != s.charAt(end--))
return false;
}
return true;
}
3、动态规划
定义二维数组dp[length][length]
,如果dp[left][right]
为true,则表示字符串从left到right是回文子串,如果dp[left][right]
为false,则表示字符串从left
到right
不是回文子串。
如果dp[left+1][right-1]
为true,我们判断s.charAt(left)
和s.charAt(right)
是否相等,如果相等,那么dp[left][right]
肯定也是回文子串,否则dp[left][right]
一定不是回文子串。
所以我们可以找出递推公式
dp[left][right]=s.charAt(left)==s.charAt(right) && dp[left+1][right-1]
有了递推公式,还要确定边界条件:
如果s.charAt(left)!=s.charAt(right)
,那么字符串从left到right是不可能构成子串的,直接跳过即可。
如果s.charAt(left)==s.charAt(right)
,字符串从left到right能不能构成回文子串还需要进一步判断
left==right
,也就是说只有一个字符,我们认为他是回文子串。即dp[left][right]=true(left==right)
right-left<=2
,类似于"aa"
,或者"aba"
,我们认为他是回文子串。即dp[left][right]=true(right-left<=2)
right-left>2
,我们只需要判断dp[left+1][right-1]
是否是回文子串,才能确定dp[left][right]
是否为true还是false。即dp[left][right]=dp[left+1][right-1]
有了递推公式和边界条件,代码就很容易写了,来看下
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param A string字符串
* @return int整型
*/
public int getLongestPalindrome(String A) {
int n = A.length();
//边界条件判断
if (n < 2)
return n;
//start表示最长回文串开始的位置,
//maxLen表示最长回文串的长度
int maxLen = 1;
boolean[][] dp = new boolean[n][n];
for (int right = 1; right < n; right++) {
for (int left = 0; left <= right; left++) {
//如果两种字符不相同,肯定不能构成回文子串
if (A.charAt(left) != A.charAt(right))
continue;
//下面是s.charAt(left)和s.charAt(right)两个
//字符相同情况下的判断
//如果只有一个字符,肯定是回文子串
if (right == left) {
dp[left][right] = true;
} else if (right - left <= 2) {
//类似于"aa"和"aba",也是回文子串
dp[left][right] = true;
} else {
//类似于"a******a",要判断他是否是回文子串,只需要
//判断"******"是否是回文子串即可
dp[left][right] = dp[left + 1][right - 1];
}
//如果字符串从left到right是回文子串,只需要保存最长的即可
if (dp[left][right] && right - left + 1 > maxLen) {
maxLen = right - left + 1;
}
}
}
//最长的回文子串
return maxLen;
}
}
思路:
对于IP地址每次取出一个数字和一个点后,对于剩余的部分可以看成是一个子问题,因此可以使用递归和回溯将点插入数字中。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param s string字符串
* @return string字符串ArrayList
*/
//记录分段IP数字字符串
private String nums = "";
public ArrayList<String> restoreIpAddresses (String s) {
// write code here
ArrayList<String> res = new ArrayList<>();
dfs(s, res, 0, 0);
return res;
}
//step 表示第几个数字,index表示字符串下标
public void dfs(String s, ArrayList<String> res, int step, int index){
//当前分割出的字符串
String cur = "";
//分割出了四个数字
if(step == 4){
//下标必须走到末尾
if(index != s.length()){
return;
}
res.add(nums);
}else{
//最长遍历三位
for(int i = index; i < index + 3 && i < s.length(); i++){
cur += s.charAt(i);
//转数字比较
int num = Integer.parseInt(cur);
String temp = nums;
//不能超过255且不能有前导0
if(num <= 255 && (cur.length() == 1 || cur.charAt(0) != '0')){
//添加点
if(step - 3 != 0){
nums += cur + ".";
}else{
nums += cur;
}
//递归查找下一位数字
dfs(s, res, step + 1, i + 1);
//回溯
nums = temp;
}
}
}
}
}
思路:
把第一个字符串变成第二个字符串,我们需要逐个将第一个字符串的子串最少操作下变成第二个字符串,这就涉及了第一个字符串增加长度,状态转移,那可以考虑动态规划。用dp[i][j]
表示从两个字符串首部各自到str1[i]
和str2[j]
为止的子串需要的编辑距离,那很明显dp[str1.length][str2.length]
就是我们要求的编辑距离。(下标从1开始)
具体做法:
初始条件
: 假设第二个字符串为空,那很明显第一个字符串子串每增加一个字符,编辑距离就加1,这步操作是删除;同理,假设第一个字符串为空,那第二个字符串每增加一个字符,编剧距离就加1,这步操作是添加。状态转移
: 状态转移肯定是将dp矩阵填满,那就遍历第一个字符串的每个长度,对应第二个字符串的每个长度。如果遍历到str1[i]
和 str2[j]
的位置,这两个字符相同,这多出来的字符就不用操作,操作次数与两个子串的前一个相同,因此有dp[i][j]=dp[i−1][j−1]
;如果这两个字符不相同,那么这两个字符需要编辑,但是此时的最短的距离不一定是修改这最后一位,也有可能是删除某个字符或者增加某个字符,因此我们选取这三种情况的最小值增加一个编辑距离,即dp[i][j]=min(dp[i−1][j−1],min(dp[i−1][j],dp[i][j−1]))+1
。public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param str1 string字符串
* @param str2 string字符串
* @return int整型
*/
public int editDistance (String str1, String str2) {
// write code here
int n = str1.length();
int m = str2.length();
//dp[i][j]
int[][] dp = new int[n + 1][m + 1];
//初始化边界
for (int i = 1; i <= n; i++) {
dp[i][0] = dp[i - 1][0] + 1;
}
for (int i = 1; i <= m; i++) {
dp[0][i] = dp[0][i - 1] + 1;
}
//遍历第一个字符串的每个位置
for (int i = 1; i <= n; i++) {
//对应第二个字符串的每个位置
for (int j = 1; j <= m; j++) {
//若是字符相同,则不用处理
if (str1.charAt(i - 1) == str2.charAt(j - 1)) {
//直接等于二者前一个的距离
dp[i][j] = dp[i - 1][j - 1];
} else {
//选取最小的距离加上此处的编辑距离 1
dp[i][j] = Math.min(dp[i - 1][j - 1],
Math.min(dp[i - 1][j], dp[i][j - 1])) + 1;
}
}
}
return dp[n][m];
}
}
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param nums int整型一维数组
* @return int整型
*/
public int rob (int[] nums) {
// write code here
//只有一个元素,就返回该元素的值
if(nums.length == 1){
return nums[0];
}
//两个元素,就返回两元素其中一个最大值
if(nums.length == 2){
return Math.max(nums[0], nums[1]);
}
//定义一个数组,n-1个值和n-2个值的和
int[] maxVal = new int[nums.length];
//初始化结果数组 第0 个元素和第1个元素
maxVal[0] = nums[0];
maxVal[1] = Math.max(nums[0],nums[1]);
for(int i = 2; i < nums.length; i++){
maxVal[i] = Math.max(maxVal[i - 1], maxVal[i - 2] + nums[i]);
}
return maxVal[maxVal.length -1];
}
}
思路:
这道题与BM78.打家劫舍(一)比较类似,区别在于这道题是环形,第一家和最后一家是相邻的,既然如此,在原先的方案中第一家和最后一家不能同时取到。
具体做法:
(初始状态)
如果数组长度为1,只有一家人,肯定是把这家人偷了,收益最大,因此dp[1]=nums[0]
。(状态转移)
每次对于一个人家,我们选择偷他或者不偷他,如果我们选择偷那么前一家必定不能偷,因此累加的上上级的最多收益,同理如果选择不偷他,那我们最多可以累加上一级的收益。因此转移方程为dp[i]=max(dp[i−1],nums[i−1]+dp[i−2])
。这里的i在dp中为数组长度,在nums中为下标。dp[1]=0
,第一家就不要了,然后遍历的时候也会遍历到数组最后一位。public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param nums int整型一维数组
* @return int整型
*/
public int rob (int[] nums) {
// write code here
//dp[i]表示长度为i的数组,最多能偷取多少钱
int[] dp = new int[nums.length + 1];
//选择偷了第一家
dp[1] = nums[0];
//最后一家不能偷
for(int i = 2; i < nums.length; i++){
//对于每家可以选择偷或者不偷
dp[i] = Math.max(dp[i - 1], nums[i - 1] + dp[i - 2]);
}
int res = dp[nums.length - 1];
//清除dp数组,第二次循环
Arrays.fill(dp, 0);
//不偷第一家
dp[1] = 0;
//可以偷最后一家
for(int i = 2; i <= nums.length; i++){
//对于每家可以选择偷或者不偷
dp[i] = Math.max(dp[i - 1], nums[i - 1] + dp[i - 2]);
}
//选择最大值返回
return Math.max(res, dp[nums.length]);
}
}
思路:
对于每天有到此为止的最大收益和是否持股两个状态,因此我们可以用动态规划。
具体做法:
dp[i][0]
表示第i天不持股到该天为止的最大收益,dp[i][1]
表示第i天持股,到该天为止的最大收益。(初始状态)
第一天不持股,则总收益为0,dp[0][0]=0
;第一天持股,则总收益为买股票的花费,此时为负数,dp[0][1]=−prices[0]
。(状态转移)
对于之后的每一天,如果当天不持股,有可能是前面的若干天中卖掉了或是还没买,因此到此为止的总收益和前一天相同,也有可能是当天才卖掉,我们选择较大的状态dp[i][0]=max(dp[i−1][0],dp[i−1][1]+prices[i])
;dp[i][1]=max(dp[i−1][1],−prices[i])
。public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param prices int整型一维数组
* @return int整型
*/
public int maxProfit (int[] prices) {
// write code here
int n = prices.length;
//dp[i][0]表示某一天不持股到该天为止的最大收益,
//dp[i][1]表示某天持股,到该天为止的最大收益
int[][] dp = new int[n][2];
//第一天不持股,总收益为0;
dp[0][0] = 0;
//第一天持股,总收益为减去该天的股价
dp[0][1] = -prices[0];
//遍历后续的每天,状态转移
for(int i = 1; i < n; i++){
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
}
//最后一天不持股,到该天为止的最大收益
return dp[n - 1][0];
}
}
这道题与BM80.买卖股票的最好时机(一)的区别在于可以多次买入卖出。
定义dp[i][0]
表示第i+1天交易完之后手里没有股票的最大利润,dp[i][1]
表示第i+1天交易完之后手里持有股票的最大利润。
当天交易完之后手里没有股票可能有两种情况,一种是当天没有进行任何交易
,又因为当天手里没有股票,所以当天没有股票的利润只能取前一天手里没有股票的利润。一种是把当天手里的股票给卖了
,既然能卖,说明手里是有股票的,所以这个时候当天没有股票的利润要取前一天手里有股票的利润加上当天股票能卖的价格。这两种情况我们取利润最大的即可,所以可以得到
dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i]);
当天交易完之后手里持有股票也有两种情况,一种是当天没有任何交易
,又因为当天手里持有股票,所以当天手里持有的股票其实前一天就已经持有了。还一种是当天买入了股票
,当天能卖股票,说明前一天手里肯定是没有股票的,我们取这两者的最大值,所以可以得到
dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i]);
动态规划的递推公式有了,那么边界条件是什么,就是第一天
如果买入:dp[0][1]=-prices[0];
如果没买:dp[0][0]=0;
有了递推公式和边界条件,代码很容易就写出来了。
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 计算最大收益
* @param prices int整型一维数组 股票每一天的价格
* @return int整型
*/
public int maxProfit (int[] prices) {
// write code here
if (prices == null || prices.length < 2) {
return 0;
}
int n = prices.length;
//dp[i][0]表示某一天不持股到该天为止的最大收益,
//dp[i][1]表示某天持股,到该天为止的最大收益
int[][] dp = new int[n][2];
//初始条件
//第一天不持股,总收益为0
dp[0][0] = 0;
//第一天持股,总收益为减去该天的股价
dp[0][1] = -prices[0];
//遍历后续每天,状态转移
for (int i = 1; i < n; i++) {
//递推公式
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
//最后一天肯定是手里没有股票的时候,利润才会最大,
//只需要返回dp[length - 1][0]即可
return dp[n - 1][0];
}
}
上面计算的时候我们看到当天的利润只和前一天有关,没必要使用一个二维数组,只需要使用两个变量,一个记录当天交易完之后手里持有股票的最大利润,一个记录当天交易完之后手里没有股票的最大利润,来看下代码
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 计算最大收益
* @param prices int整型一维数组 股票每一天的价格
* @return int整型
*/
public int maxProfit (int[] prices) {
// write code here
if (prices == null || prices.length < 2) {
return 0;
}
int n = prices.length;
//初始条件
//没持有股票
int noHold = 0;
//持有股票
int hold = -prices[0];
//遍历后续每天,状态转移
for (int i = 1; i < n; i++) {
//递推公式转化的
noHold = Math.max(noHold, hold + prices[i]);
hold = Math.max(hold, noHold - prices[i]);
}
//最后一天肯定是手里没有股票的时候,利润才会最大,
//所以这里返回的是noHold
return noHold;
}
}
思路:
这道题与BM80.买卖股票的最好时机(一)的区别在于最多可以买入卖出2次,那实际上相当于它的状态多了几个,对于每天有到此为止的最大收益和持股情况两个状态,持股情况有了5种变化,我们用:
dp[i][0]
表示到第i天为止没有买过股票的最大收益dp[i][1]
表示到第i天为止买过一次股票还没有卖出的最大收益dp[i][2]
表示到第i天为止买过一次也卖出过一次股票的最大收益dp[i][3]
表示到第i天为止买过两次只卖出过一次股票的最大收益dp[i][4]
表示到第i天为止买过两次同时也买出过两次股票的最大收益于是使用动态规划,有了如下的状态转移
具体做法:
初始状态
:与上述提到的题类似,第0天有买入了和没有买两种状态:dp[0][0]=0
、dp[0][1]=−prices[0]
。状态转移
:对于后续的每一天,如果当天还是状态0,则与前一天相同,没有区别;dp[i][1]=max(dp[i−1][1],dp[i−1][0]−prices[i])
;dp[i][2]=max(dp[i−1][2],dp[i−1][1]+prices[i])
;dp[i][3]=max(dp[i−1][3],dp[i−1][2]−prices[i])
;
dp[i][4]=max(dp[i−1][4],dp[i−1][3]+prices[i])
。public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 两次交易所能获得的最大收益
* @param prices int整型一维数组 股票每一天的价格
* @return int整型
*/
public int maxProfit (int[] prices) {
// write code here
int n = prices.length;
int[][] dp = new int[n][5];
//初始化dp为最小
Arrays.fill(dp[0], -10000);
//第0天不持有状态
dp[0][0] = 0;
//第0天持有股票
dp[0][1] = -prices[0];
//状态转移
for (int i = 1; i < n; i++) {
dp[i][0] = dp[i - 1][0];
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
}
//选取最大值,可以只操作一次
return Math.max(dp[n - 1][2], Math.max(0, dp[n - 1][4]));
}
}
思路:
将单词位置的反转,那肯定前后都是逆序,不如我们先将整个字符串反转,这样是不是单词的位置也就随之反转了。但是单词里面的成分也反转了啊,既然如此我们再将单词里面的部分反转过来就行。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param s string字符串
* @param n int整型 -
4,mn
* @return string字符串
*/
public String trans (String s, int n) {
// write code here
if (n == 0)
return s;
StringBuffer res = new StringBuffer();
for (int i = 0; i < n; i++) {
//大小写转换
if (s.charAt(i) >= 'A' && s.charAt(i) <= 'Z'){
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++;
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();
}
}
纵向扫描
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param strs string字符串一维数组
* @return string字符串
*/
public String longestCommonPrefix (String[] strs) {
// write code here
//纵向扫描
if(strs.length == 0 || strs == null){
return "";
}
int rows = strs.length;
int cols = strs[0].length();
//开始扫描
for(int i = 0; i < cols; i++){
//拿到每一列的第一个字符
char firstChar = strs[0].charAt(i);
//遍历比较
for(int j = 1; j < rows; j++){
//如果遍历到的字符跟第一个字符不相等,就结束
if(strs[j].length() == i || strs[j].charAt(i)!=firstChar){
return strs[0].substring(0, i);
}
}
}
return strs[0];
}
}
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 验证IP地址
* @param IP string字符串 一个IP地址字符串
* @return string字符串
*/
public String solve (String IP) {
// 判断是否为IPv4地址,是则返回"IPv4",否则继续验证是否为IPv6地址
return validIPv4(IP) ? "IPv4" : (validIPv6(IP) ? "IPv6" : "Neither");
}
/**
* 验证IPv4地址
* @param IP string字符串 一个IP地址字符串
* @return boolean值,表示是否为有效的IPv4地址
*/
private boolean validIPv4(String IP) {
// 将IP地址按照"."进行拆分,得到字符串数组
String[] strs = IP.split("\\.", -1);
// 如果拆分后的数组长度不为4,则说明IP地址无效
if (strs.length != 4) {
return false;
}
// 遍历每个部分进行验证
for (String str : strs) {
// 如果部分长度大于1且以0开头,则说明有不合法的前导零
if (str.length() > 1 && str.startsWith("0")) {
return false;
}
try {
// 将部分转换为整数
int val = Integer.parseInt(str);
// 判断整数是否在有效范围内(0到255之间)
if (!(val >= 0 && val <= 255)) {
return false;
}
} catch (NumberFormatException numberFormatException) {
// 如果转换出现异常,则说明部分不是有效的整数
return false;
}
}
// IP地址为有效的IPv4地址
return true;
}
/**
* 验证IPv6地址
* @param IP string字符串 一个IP地址字符串
* @return boolean值,表示是否为有效的IPv6地址
*/
private boolean validIPv6(String IP) {
// 将IP地址按照":"进行拆分,得到字符串数组
String[] strs = IP.split(":", -1);
// 如果拆分后的数组长度不为8,则说明IP地址无效
if (strs.length != 8) {
return false;
}
// 遍历每个部分进行验证
for (String str : strs) {
// 如果部分长度超过4或者长度为0,则说明不是有效的IPv6地址
if (str.length() > 4 || str.length() == 0) {
return false;
}
try {
// 将部分作为16进制数转换为整数
int val = Integer.parseInt(str, 16);
} catch (NumberFormatException numberFormatException) {
// 如果转换出现异常,则说明部分不是有效的整数
return false;
}
}
// IP地址为有效的IPv6地址
return true;
}
}
思路:
大整数相加,就可以按照整数相加的方式,从个位开始,逐渐往上累加,换到字符串中就是从两个字符串的末尾开始相加。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 计算两个数之和
* @param s string字符串 表示第一个整数
* @param t string字符串 表示第二个整数
* @return string字符串
*/
public String solve (String s, String t) {
// write code here
//若是其中一个为空,返回另一个
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,B进行合并
将合并后的数组A进行排序
import java.util.*;
public class Solution {
public void merge(int A[], int m, int B[], int n) {
//合并
for(int i=0;i!=n;i++){
A[m+i] = B[i];
}
//排序
Arrays.sort(A);
}
}
解法二:双指针
思路步骤
public class Solution {
/**
* 合并两个有序数组
*
* @param A 数组A,长度为m
* @param m 数组A的有效元素个数
* @param B 数组B,长度为n
* @param n 数组B的有效元素个数
*/
public void merge(int A[], int m, int B[], int n) {
// 定义两个指针,分别指向数组A和数组B的起始位置
int p1 = 0, p2 = 0;
// 新开一个大小为m+n的数组用于存放合并后的结果
int[] sorted = new int[m + n];
int cur; // 用于记录选择出来的较小的元素
// 循环选择较小的元素放入新数组
while (p1 < m || p2 < n) {
if (p1 == m) {
// 如果数组A的元素已经全部选择完,则直接选择数组B的元素
cur = B[p2++];
} else if (p2 == n) {
// 如果数组B的元素已经全部选择完,则直接选择数组A的元素
cur = A[p1++];
} else if (A[p1] < B[p2]) {
// 如果数组A的当前元素小于数组B的当前元素,则选择数组A的元素
cur = A[p1++];
} else {
// 否则选择数组B的元素
cur = B[p2++];
}
sorted[p1 + p2 - 1] = cur; // 将选择的元素放入新数组中
}
// 将新数组中的元素复制回原数组A
for (int i = 0; i != m + n; ++i) {
A[i] = sorted[i];
}
}
}
复杂度分析:
时间复杂度: 。
指针移动单调递增,最多移动 m+n次。
空间复杂度:
需要建立长度为 m+n的中间数组sorted
解法一:双指针
回文字符串正向遍历与逆向遍历结果都是一样的,因此我们可以准备两个对撞指针,一个正向遍历,一个逆向遍历。
具体做法:
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;
}
}
解法二:反转字符串比较法
思路:
既然字符串正向遍历与逆向遍历遇到字符都相等,那我们就反转字符串,看看它到底是不是正逆都一样,因为字符串支持整体比较,因此我们可以比较反转后的字符串与原串是不是相等。
具体做法:
import java.util.*;
public class Solution {
public boolean judge (String str) {
StringBuffer temp = new StringBuffer(str);
//反转字符串
String s = temp.reverse().toString();
//比较字符串是否相等
if(s.equals(str))
return true;
return false;
}
}
public class Solution {
/**
* 合并区间
*
* @param intervals 区间列表,类型为ArrayList
* @return 合并后的区间列表,类型为ArrayList
*/
public ArrayList<Interval> merge(ArrayList<Interval> intervals) {
// 创建一个用于存放合并结果的列表
ArrayList<Interval> result = new ArrayList<>();
// 如果输入为空或只有一个区间,则无需合并,直接返回原列表
if (intervals == null || intervals.size() < 2) {
return intervals;
}
// 按照区间起始位置进行排序,以保证相邻区间之间的关系
Collections.sort(intervals, (v1, v2) -> v1.start - v2.start);
int index = -1; // 记录结果列表中最后一个区间的索引
// 遍历整个区间列表
for (Interval interval : intervals) {
if (index == -1 || interval.start > result.get(index).end) {
// 如果当前区间和前一个区间没有重叠,则将当前区间直接添加到结果列表中
result.add(interval);
index++;
} else {
// 如果当前区间和前一个区间有重叠,则更新前一个区间的结束位置
result.get(index).end = Math.max(interval.end, result.get(index).end);
}
}
return result;
}
}
使用StringBuilder,一行代码搞定
public String solve(String str) {
return new StringBuilder(str).reverse().toString();
}
双指针:
把字符串转化为数组,使用两个指针,一个在最前面一个在最后面,两个指针指向的值相互交换,交换完之后两个指针在分别往中间走……,重复上面的过程,直到两指针相遇为止
public String solve(String str) {
char[] chars = str.toCharArray();
int left = 0;
int right = str.length() - 1;
while (left < right) {
char temp = chars[left];
chars[left] = chars[right];
chars[right] = temp;
left++;
right--;
}
return new String(chars);
}
我们使用两个指针,一个i一个j,最开始的时候i和j指向第一个元素,然后i往后移,把扫描过的元素都放到map中,如果i扫描过的元素没有重复的就一直往后移,顺便记录一下最大值max
,如果i扫描过的元素有重复的,就改变j的位置,画个图看一下(数字和字符串原理一样)
public class Solution {
/**
* 无重复字符的最长子数组长度
*
* @param arr int类型的一维数组
* @return int类型,表示最长子数组的长度
*/
public int maxLength(int[] arr) {
// 如果输入数组为空,则最长子数组长度为0
if (arr.length == 0) {
return 0;
}
// 用于存储元素和对应的下标
HashMap<Integer, Integer> map = new HashMap<>();
// 记录最长子数组的长度
int max = 0;
for (int i = 0, j = 0; i < arr.length; i++) {
if (map.containsKey(arr[i])) {
// 如果当前元素在map中已经存在,更新窗口的起始位置j
//map.get(arr[i])拿到的是前一个元素,j应该往后一步
j = Math.max(j, map.get(arr[i]) + 1);
}
// 将当前元素和下标放入map中
map.put(arr[i], i);
// 更新最长子数组的长度
max = Math.max(max, i - j + 1);
}
return max;
}
}
我们还可以用一个队列,把元素不停的加入到队列中,如果有相同的元素,就把队首的元素移除,这样我们就可以保证队列中永远都没有重复的元素
public int maxLength(int[] arr) {
//用链表实现队列,队列是先进先出的
Queue<Integer> 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;
}
题目主要信息:
方法:贪心法(建议使用)
知识点1:双指针
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
知识点2:贪心思想
贪心思想属于动态规划思想中的一种,其基本原理是找出整体当中给的每个局部子结构的最优解,并且最终将所有的这些局部最优解结合起来形成整体上的一个最优解。
思路:
这道题利用了水桶的短板原理,较短的一边控制最大水量
,因此直接用较短边长乘底部两边距离就可以得到当前情况下的容积。但是要怎么找最大值呢?
可以利用贪心思想:我们都知道容积与最短边长和底边长有关,最长的底边一定以首尾为边,但是首尾不一定够高,中间可能会出现更高但是底边更短的情况,因此我们可以使用对撞双指针向中间靠,这样底边长会缩短,因此还想要有更大容积只能是增加最短边长,此时我们每次指针移动就移动较短的一边,因为贪心思想下较长的一边比较短的一边更可能出现更大容积。
//优先舍弃较短的边
if(height[left] < height[right])
left++;
else
right--;
具体做法:
图示:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param height int整型一维数组
* @return int整型
*/
public int maxArea (int[] height) {
// write code here
//首先排除不能形成容器的情况
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 ++;
}else{
right --;
}
}
return res;
}
}
思路:
我们利用贪心思想,什么时候需要的主持人最少?那肯定是所有的区间没有重叠,每个区间首和上一个的区间尾都没有相交的情况,我们就可以让同一位主持人不辞辛劳,一直主持了。但是题目肯定不是这种理想的情况,那我们需要对交叉部分,判断需要增加多少位主持人。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 计算成功举办活动需要多少名主持人
* @param n int整型 有n个活动
* @param startEnd int整型二维数组 startEnd[i][0]用于表示第i个活动的开始时间,startEnd[i][1]表示第i个活动的结束时间
* @return int整型
*/
public int minmumNumberOfHost (int n, int[][] startEnd) {
// write code here
//利用辅助数组获取单独各个活动开始的时间和结束时间
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;
}
}
题目主要信息:
思路:
循环右移相当于从第m个位置开始,左右两部分视作整体翻转
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 旋转数组
* @param n int整型 数组长度
* @param m int整型 右移距离
* @param a int整型一维数组 给定数组
* @return int整型一维数组
*/
public int[] solve (int n, int m, int[] a) {
// write code here
//取余,因为每次长度为n的旋转数组相当于没有变化
m = m % n;
//第一次逆转全部数组元素
reverse(a, 0, n - 1);
//第二次只逆转开头m个
reverse(a, 0, m - 1);
//第三次只逆转结尾m个
reverse(a, m, n - 1);
return a;
}
//反转函数
public void reverse(int[] nums, int start, int end) {
while (start < end) {
swap(nums, start++, end--);
}
}
//交换函数
public void swap(int[] nums, int a, int b) {
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
思路:
这道题就是一个简单的模拟,我们想象有一个矩阵,从第一个元素开始,往右到底后再往下到底后再往左到底后再往上,结束这一圈,进入下一圈螺旋。
具体做法:
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param matrix int整型二维数组
* @return int整型ArrayList
*/
public ArrayList<Integer> spiralOrder (int[][] matrix) {
// write code here
ArrayList<Integer> res = new ArrayList<>();
//先排除特殊情况
if (matrix.length == 0) {
return res;
}
//左边界
int left = 0;
//右边界
int right = matrix[0].length - 1;
//上边界
int up = 0;
//下边界
int down = matrix.length - 1;
//直到边界重合
while (left <= right && up <= down) {
//上边界的从左到右
for (int i = left; i <= right; i++) {
res.add(matrix[up][i]);
}
//上边界向下
up++;
if (up > down) {
break;
}
//右边界的从上到下
for (int i = up; i <= down; i++) {
res.add(matrix[i][right]);
}
//右边界向左
right--;
if (left > right)
break;
//下边界的从右到左
for (int i = right; i >= left; i--)
res.add(matrix[down][i]);
//下边界向上
down--;
if (up > down)
break;
//左边界的从下到上
for (int i = down; i >= up; i--)
res.add(matrix[i][left]);
//左边界向右
left++;
if (left > right)
break;
}
return res;
}
}
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param mat int整型二维数组
* @param n int整型
* @return int整型二维数组
*/
public int[][] rotateMatrix (int[][] mat, int n) {
// write code here
int length = mat.length;
//先上下交换
for (int i = 0; i < length / 2; i++) {
int temp[] = mat[i];
mat[i] = mat[length - i - 1];
mat[length - i - 1] = temp;
}
//在按照对角线交换
for (int i = 0; i < length; ++i) {
for (int j = i + 1; j < length; ++j) {
int temp = mat[i][j];
mat[i][j] = mat[j][i];
mat[j][i] = temp;
}
}
return mat;
}
}