链表问题
从指定位置反转链表
92题
要反转这个链表我们至少需要以下几个变量:
- pre:反转链表段的前一个节点
- cur:反转链表的头
在while循环中,next先指向还没有发生反转的链表的下一个节点,所以最后next会保存5的位置
public ListNode reverseBetween(ListNode head, int left, int right) {
if(left==right)return head;
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode pre=dummy;
ListNode cur=dummy.next;
//移动left次,使得pre对应于反转链表段的前一个
for (int i = 0; i < left-1 ; i++) {
pre=pre.next;
cur=cur.next;
}
ListNode subDummy=pre;//保存用于连接链表段
ListNode subHead=cur;//保存用于后续连接尾部
//后移准备操作链表反转
pre=pre.next;
cur=cur.next;
//此操作对right-left格链表执行
ListNode next=null;
for (int i = 0; i < right - left; i++) {
next=cur.next;
cur.next=pre;
pre=cur;
cur=next;
}
subDummy.next=pre;
subHead.next=next;
return dummy.next;
}
分隔链表
86题
搜索每个节点,分别保存小于该节点的链表头和大于该节点的链表头,每当有节点小于x就加在smallHead的后面,反之亦然,最后需要注意的是large.next需要断开,因为如图上述情况5并不是尾结点,不断开的话会形成环
public ListNode partition(ListNode head, int x) {
ListNode small=new ListNode(0);
ListNode smallHead=small;
ListNode large=new ListNode(0);
ListNode largeHead=large;
while (head!=null){
if(head.val<x){
small.next=head;
small=small.next;
}else{
large.next=head;
large=large.next;
}
head=head.next;
}
large.next=null;
small.next=largeHead.next;
return smallHead.next;
}
删除排序链表中的重复元素
82题
使用pre记录一个前置节点,当重复节点出现的时候让cur不停的往后移动,仅当pre和cur中间没有节点的时候移动pre的位置,不然就讲重复的节点直接删除
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) return head;
ListNode dummy=new ListNode(0);
dummy.next=head;
ListNode pre=dummy;
ListNode cur=head;
while (cur!=null){
while (cur.next!=null&&cur.val==cur.next.val){
cur=cur.next;
}
//pre和cur之间没有重复的节点
if(pre.next==cur){
pre=pre.next;
}else{
pre.next=cur.next;//删除重复节点
}
//遍历下一个节点
cur=cur.next;
}
return dummy.next;
}
旋转链表
61题
先将链表连成一个环,然后再指定的地方断开,其中移动的次数为n-k,由于k可能大于n所以要对k进行取模运算,移动后iter位于头的前一个位置(也就是当前环的尾部),因为iter开始是从尾部开始移动的,
//先把链表变成环,再找断开的地方就行了
public ListNode rotateRight(ListNode head, int k) {
//特殊情况排除
if (k == 0 || head == null || head.next == null) {
return head;
}
//记录链表长度
int n = 1;
ListNode iter = head;
while (iter.next != null) {
iter = iter.next;
n++;
}
//需要移动的次数
int add = n - k % n;
//如果刚好为n说明不需要移动,直接return就好了
if (add == n) {
return head;
}
//链表成环,当前iter位于链表的尾部
iter.next = head;
//移动到环的尾部
while (add-- > 0) {
iter = iter.next;
}
//记录链表的头
ListNode ret = iter.next;
//尾部指向null
iter.next = null;
return ret;
}
删除链表的倒数第N个节点
19题
使用快慢指针解决此问题,先用fast指针探路,如果在n次位移以后fast已经是null了说明n>链表的总长度,说明需要删除头节点,此时直接return head.next,此题难点就在于此,需要将头结点删除的情况单独分离开来
如果不为null,fast指针和slow指针一起走,当fast指针的下一个为null的时候slow正处于将要被删除节点的前一个节点,
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode fast=head,slow=head;
while(n!=0){
fast=fast.next;
n--;
}
if(fast==null)return head.next;
while (fast.next!=null){
fast=fast.next;
slow=slow.next;
}
slow.next=slow.next.next;
return head;
}
合并两个有序链表
21题
用递归来做的话代码更为简洁
//给定两个链表返回最小的头
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
//两个函数都为空的时候俩表合并完成
if(l1==null)return l2;
else if(l2==null) return l1;
//判断当前哪个头结点更小,使用较小的头结点next指针指向其余节点的合并结果
else if(l1.val<l2.val){
l1.next=mergeTwoLists(l1.next,l2);
return l1;
}else{
l2.next=mergeTwoLists(l1,l2.next);
return l2;
}
}
合并k个升序的链表
23题
使用一个优先队列来帮助我们做就很简单了,把所有的节点都丢进优先队列里去然后重新组装链表就行了
public ListNode mergeKLists(ListNode[] lists) {
if(lists==null||lists.length==0)return null;
PriorityQueue<ListNode> pq = new PriorityQueue<>(new Comparator<ListNode>() {
@Override
public int compare(ListNode l1, ListNode l2) {
return l1.val - l2.val;
}
});
for (int i = 0; i < lists.length; i++) {
ListNode now=lists[i];
while (now!=null){
pq.offer(now);
now=now.next;
}
}
ListNode last;
ListNode res;
if(pq.size()>=1){
last = pq.poll();
res =last;
}else{
return null;
}
while (!pq.isEmpty()) {
ListNode poll = pq.poll();
last.next=poll;
last=poll;
}
last.next=null;
return res;
}
两两交换链表中的节点
24题
终止条件为,只有一个节点或者没有节点,swapPairs这个函数会返回链表交换后节点的头
//给定一个链表,返回交换完成的子链表
public ListNode swapPairs(ListNode head) {
//递归的终止条件,只有一个节点或者没有节点无法完成交换
if (head == null || head.next == null) {
return head;
}
ListNode one = head;
ListNode two = one.next;
ListNode three = two.next;
two.next = one;
one.next = swapPairs(three);
return two;
}
k个一组翻转链表
25题
pre:维护成需要翻转链表的头节点的上一个节点
end:维护成需要翻转链表的尾结点(翻转后会变成头节点)--当end==null的时候说明不足k次直接跳出循环不处理就行
next:维护成end翻转前的后一个节点用于后续连接链表
start:维护成翻转链表的头节点(翻转后变成了尾节点)--没轮循环开始将pre和end指向start因为它也是下一段该翻转链表的前一个节点
public ListNode reverseKGroup(ListNode head, int k) {
if(head==null||head.next==null)return head;
ListNode dummy=new ListNode(0);//头结点前的节点方便返回操作
dummy.next=head;
ListNode pre=dummy;//以后会指向每次要翻转的链表段头结点的上一个节点。
ListNode end=dummy;//以后会指向每次要翻转的链表的尾节点
while(end.next!=null){
for (int i = 0; i < k &&end!=null; i++) {//循环k次找到end的位置,k=2的时候end往后移动2次
end=end.next;
}
//end==null说明翻转的链表节点数小于k
if(end==null){
break;
}
//记录end.next方便后面连接链表
ListNode next=end.next;
//断开链表
end.next=null;
//记录要翻转的链表的头结点
ListNode start=pre.next;
//翻转链表 dummy-1-2 -> dummy-2-1
pre.next=reverse(start);
//翻转后我们需要把头结点编导最后把断开的链表重新连接
start.next=next;
//将pre缓存下次要翻转的链表的头结点的上一个节点
pre=start;
//翻转结束,将end变为下次要翻转的链表的头结点的上一个节点,即start
end=start;
}
return dummy.next;
}
public ListNode reverse(ListNode head){
//单链表为空或者只有一个节点,直接返回
if(head==null||head.next==null)return head;
//前一个节点指针
ListNode preNode=null;
//当前节点指针
ListNode curNode= head;
//下一个节点指针
ListNode nextNode=null;
while(curNode!=null){
nextNode=curNode.next;//nextNode指向下一个节点,保存当前节点后面的链表
curNode.next=preNode;//将当前节点next指向前一个节点
preNode=curNode;//pre指针向后移动,指向当前节点
curNode=nextNode;//cur向后移动,指向后一个节点
}
return preNode;
}
解码方法
91题
此题也是用动态规划做,当前导为0的时候,s是无解的
一个字符一个字符的读,我们默认每个状态都是可编译的,所以一开始先继承上次的状态last_1
- 当当前数字为0的时候如果前驱是1或者2,由于它只能是10或者20了所以需要往前移动两次,使用last_2来记录,否则无解
- 当数前驱数字为1时,当前数字可以为任意值
- 当前驱数字为2时,当前数字只能是1-6的一个,该情况与1的情况一致,数字可以拆分成两个单独的或者一个整体,因为我们默认temp继承了last_1,所以它已经继承了单独的状态,只要将last_2合并的状态加上来就行了
int numDecodings(string s) {
if (s[0] == '0') return 0;
vector<int> dp(s.size()+1);
dp[0]=1;dp[1]=1;
for (int i =1; i < s.size(); i++) {
if (s[i] == '0')//1.s[i]为0的情况
if (s[i - 1] == '1' || s[i - 1] == '2') //s[i - 1]等于1或2的情况
dp[i+1] = dp[i-1];//由于s[1]指第二个下标,对应为dp[2],所以dp的下标要比s大1,故为dp[i+1]
else
return 0;
else //2.s[i]不为0的情况
if (s[i - 1] == '1' || (s[i - 1] == '2' && s[i] <= '6'))//s[i-1]s[i]两位数要小于26的情况
dp[i+1] = dp[i]+dp[i-1];
else//其他情况
dp[i+1] = dp[i];
}
return dp[s.size()];
}
复杂度简化
public int numDecodings(String s) {
if(s.charAt(0) == '0') {
//说明无解
return 0;
}
char[] charArray = s.toCharArray();
int last_2 = 1, last_1 = 1; //last_2 代表i-2 last_1 代表i-1 temp 代表当前
for(int i=1; i<s.length(); i++) {
int temp = last_1;//正常情况就是继承上一次的状态,我们默认它为可编译的
if(charArray[i] == '0') {
//如果是10这种情况,相当于减去当前的两个字符,继承i-2的状态
if(charArray[i-1] == '1' || charArray[i-1] == '2') {
temp = last_2;
}
//30这种情况也是无解的
else {
return 0;
}
}else if( charArray[i-1] == '1' || (charArray[i-1] == '2' && charArray[i] - '0'>0 && charArray[i] - '0'<7)) {
temp += last_2;//现在构成了12这样的状态,他可以拆分成1 2 和12两种,temp当前为last_1已经包含了1 2的拆分的状态,还缺少12的合并状态,所以从i-2继承
}
//状态更新
last_2 = last_1;
last_1 = temp;
}
return last_1;
}
合并两个有序的数组
88题
从后向前开始合并,这样的题不要用for循环做,用多个指针就很简单
public void merge(int[] nums1, int m, int[] nums2, int n) {
int index = m + n;
while (n>0){
if(m>0&&nums1[m-1]>nums2[n-1]){
nums1[--index]=nums1[--m];
}else{
nums1[--index]=nums2[--n];
}
}
}
最大的矩形
柱状图中的最大矩形
84题
这样边界不好判断的题目最好加入一个哨兵,这样就可以避免一些非空的判断,在筛除完特殊情况之后,对此类的题的数组进行一个修改使其变成0 ,原数组,0的形式
对于java的数组复制,一共有四个api
object类的clone方法
Arrays的copyOfRange() 方法:第2,3个参数是from,to左开右闭
int[] original = new int[]{1, 2, 3, 4, 5}; int[] dest1 = Arrays.copyOfRange(original, 0, 8);//12345000 int[] dest2 = Arrays.copyOfRange(original, 1, 4);//234
Arrays的copyOf()方法,第二个参数是数组长度
int[] original = new int[]{1, 2, 3, 4, 5}; int[] dest1 = Arrays.copyOf(original, 8);//12345000 int[] dest2 = Arrays.copyOf(original, 3);//123
System的arraycopy方法,这是唯一可以修改数组前导的方法,其参数为:
原始数组.原始数组开始复制的位置,目标数组,目标数组的初始位置,复制的长度
int[] src = new int[]{1, 2, 3}; int[] des = new int[]{1, 2, 3, 0, 0, 0, 0}; System.arraycopy(src, 0, des, 3, 3);//1, 2, 3, 1, 2, 3, 0
在此题中我们发现最大长方形的面积只在高度减少的时候计算,比如高度为5的最大长方形要在6的地方计算为5*2;我们向栈中存放索引,因为索引既可以拿到柱状图的高度又可以拿到柱状图的宽度,值得注意的是这个栈还是一个索引的单调栈,因此宽度永远不可能为负数,在主要逻辑运行之前,向栈中存一个0索引
在主要逻辑运行的时候,不停的向栈中压入新数组的值,当当前遍历的值要小于栈顶的值的时候开始计算矩形的长度,由于我们是从低到高记录的,所以在往回推算面积的时候只需要取出该层的高度并且求出索引差(也就是宽度),就可以得到面积
在栈中存在的倒数第二个值是索引高度为1的索引下标,由于它是最矮的值,所以它需要计算全部的长度,这也是在数组中存放前导和后导0的原因
我们在栈中抛出的都是高度比当前i更高的索引值(或者说每次计算完一次面积,就把该高度从栈中一移除),所以不用担心值抛出去但是面积计算错误的情况,举个例子 在计算高度为2的索引的时候,高度为5和6的索引已经被抛出去了,但是这并不对2的面积计算造成影响,因为它只需要找到高度为1的索引位置计算面积就行了
public int largestRectangleArea(int[] heights) {
int len = heights.length;
//特殊情况
if (len == 0) {
return 0;
}
if (len == 1) {
return heights[0];
}
int res = 0;
int[] newHeights = new int[len + 2];
newHeights[0] = 0;
System.arraycopy(heights, 0, newHeights, 1, len);
newHeights[len + 1] = 0;
len += 2;
heights = newHeights;//新数组 0 原数组 0
Deque<Integer> stack = new ArrayDeque<>(len);
// 先放入哨兵,在循环里就不用做非空判断
stack.addLast(0);//栈中存的是索引
//这一段的总体逻辑是栈里面只存递增高度的下标,遇到比自己小的就开始往前计算以自己为高度的每个矩形的面积
for (int i = 1; i < len; i++) {
//遍历到的高度比当前栈中高度要小的时候开始计算矩形
while (heights[i] < heights[stack.peekLast()]) {
int curHeight = heights[stack.pollLast()]; //取出高度
int curWidth = i - stack.peekLast() - 1; //计算长度
res = Math.max(res, curHeight * curWidth);
}
stack.addLast(i);
}
return res;
}
二维数组中的最大矩形
85题
对每一层构造一个数组,题目其实就和上一题一样,由于是逐层遍历下来的不必担心有悬在空中的情况,因为这种情况会被上一层计算到
public int maximalRectangle(char[][] matrix) {
int m=matrix.length;
if(m==0)return 0;
int n=matrix[0].length;
int[] heights = new int[n];
int maxArea=0;
//从上到下遍历构造出高度数组
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if(matrix[i][j]=='1'){
heights[j]+=1;
}else{
heights[j]=0;
}
}
maxArea=Math.max(maxArea,largestRectangleArea(heights));
}
return maxArea;
}
public int largestRectangleArea(int[] heights) {
int len = heights.length;
//特殊情况
if (len == 0) {
return 0;
}
if (len == 1) {
return heights[0];
}
int res = 0;
int[] newHeights = new int[len + 2];
newHeights[0] = 0;
System.arraycopy(heights, 0, newHeights, 1, len);
newHeights[len + 1] = 0;
len += 2;
heights = newHeights;//新数组= 0+原数组+0
Deque<Integer> stack = new ArrayDeque<>(len);
// 先放入哨兵,在循环里就不用做非空判断
stack.addLast(0);//栈中存的是索引
//这一段的总体逻辑是栈里面只存递增高度的下标,遇到比自己小的就开始往前计算以自己为高度的每个矩形的面积
for (int i = 1; i < len; i++) {
//遍历到的高度比当前栈中高度要小的时候开始计算矩形
while (heights[i] < heights[stack.peekLast()]) {
int curHeight = heights[stack.pollLast()]; //取出高度
int curWidth = i - stack.peekLast() - 1; //计算长度
res = Math.max(res, curHeight * curWidth);
}
stack.addLast(i);
}
return res;
}
删除有序数组中的重复项
80题,每个元素最多出现两次
我们使用一个通用的解法,无论以后只能出现3次还是4次都可以用此方法求解;
举个例子对于0011112222来说,如果只保留两个数字的话,算法会如此运行
首先保留00,而后对于1与之前的0不同,所以保留,但是遍历到第三个1的时候因为第三个1的两个数之前也是1,所以不保留故而得001122
public int removeDuplicates(int[] nums) {
return process(nums, 2);
}
//通用解法
int process(int[] nums, int k) {
int u = 0;
for (int x : nums) {
//对于前k个数字我们可以直接保留
//对于k个数字以后的数字与k个数字以前的数字比较,不同则保留
if (u < k || nums[u - k] != x) nums[u++] = x;
}
return u;
}
快速排序的子过程
75题,颜色分类
其实就是利用zero和two两个指针,在遍历过程中遍历到0就与zero指针所在的值交换,遍历到2就与two指针所在的值交换,zero指针一致加,two指针一直减少,当i和two指针相等的时候说明遍历已经完成;
需要注意的是0和1一定在2的前面,所以nums[i]=0的时候要丢到前面去,前面已经是排序好的值,不需要再次操作,所以i++,nums[i]=1的时候,当前的值不需要改变,因为它迟早会被0挤下来,所以直接遍历下一个数字;,而遍历到2的时候是要把2丢到后面去的,丢到后面去以后,当前位置会进来一个新的值,所以不需要i++;
public void sortColors(int[] nums) {
int len = nums.length;
if (len < 2) {
return;
}
// all in [0, zero) = 0
// all in [zero, i) = 1
// all in [two, len - 1] = 2
// 循环终止条件是 i == two,那么循环可以继续的条件是 i < two
// 为了保证初始化的时候 [0, zero) 为空,设置 zero = 0,
// 所以下面遍历到 0 的时候,先交换,再加
int zero = 0;
// 为了保证初始化的时候 [two, len - 1] 为空,设置 two = len
// 所以下面遍历到 2 的时候,先减,再交换
int two = len;
int i = 0;
// 当 i == two 上面的三个子区间正好覆盖了全部数组
// 因此,循环可以继续的条件是 i < two
while (i < two) {
if (nums[i] == 0) {
swap(nums, i, zero);
zero++;
i++;
} else if (nums[i] == 1) {
i++;
} else {
two--;
swap(nums, i, two);
}
}
}
private void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
搜索二维矩阵
74题
从每行的第一个数字可以大概确定target的范围,从最后一行开始搜索,如果第一个数字比当前数字大就减少行数,否则增加列数
public boolean searchMatrix(int[][] matrix, int target) {
int rows = matrix.length - 1, columns = 0;
while (rows >= 0 && columns < matrix[0].length) {
int num = matrix[rows][columns];
if (num == target) {
return true;
} else if (num > target) {
rows--;
} else {
columns++;
}
}
return false;
}
矩阵置零
73题
由于要把零所在的行和列都置为9,所以我们先进行一次遍历,把所有有0的行和列都标记出来,使用第一行和第一列来标志,但是要注意排查第一行和第一列有零的情况
public void setZeroes(int[][] matrix) {
int row = matrix.length;
int col = matrix[0].length;
boolean row0_flag = false;
boolean col0_flag = false;
// 第一行是否有零
for (int j = 0; j < col; j++) {
if (matrix[0][j] == 0) {
row0_flag = true;
break;
}
}
// 第一列是否有零
for (int i = 0; i < row; i++) {
if (matrix[i][0] == 0) {
col0_flag = true;
break;
}
}
// 把第一行第一列作为标志位
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
if (matrix[i][j] == 0) {
matrix[i][0] = matrix[0][j] = 0;
}
}
}
// 置0
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}
if (row0_flag) {
for (int j = 0; j < col; j++) {
matrix[0][j] = 0;
}
}
if (col0_flag) {
for (int i = 0; i < row; i++) {
matrix[i][0] = 0;
}
}
简化路径
71题
关于路径的题目可以用一个栈来模拟,预先使用/来分割每个路径串,..其实是返回上一个路径,其实也就是出栈嘛,如果不是..那就直接入栈呗,最后把栈里的每个元素拿出来加上/不就成了嘛;注意这里的stack的遍历方式,因为是倒着添加的,所以需要从后往前拿
public String simplifyPath(String path) {
Deque<String> stack = new LinkedList<>();
StringBuilder ret = new StringBuilder();
for (String p : path.split("/")) {
if (!stack.isEmpty() && p.equals("..")) {
stack.removeFirst();
} else if (!" ..".contains(p)) {
stack.push(p);
}
}
while (!stack.isEmpty()){
ret.append("/"+stack.removeLast());
}
return ret.length() == 0 ? "/" : ret.toString();
}
寻找平方根
69题
刨除掉特殊情况1和0,x的平方根在1-1/2*x之间 ,随意我们可以用二分法在这个范围内筛选结果,筛选过程中有可能会出现整形溢出情况,改用除法运算,由于最后的结果是要靠近小的那个数(2.2约成2),所以将压缩左边界的情况和等于的情况放在一起,然后返回left
public int mySqrt(int x) {
// 特殊值判断
if (x == 0) {
return 0;
}
if (x == 1) {
return 1;
}
int left = 1;
int right = x / 2;
// 在区间 [left..right] 查找目标元素
while (left < right) {
int mid = left + (right - left + 1) / 2;
// 注意:这里为了避免乘法溢出,改用除法
if (mid > x / mid) {
// 下一轮搜索区间是 [left..mid - 1]
right = mid - 1;
} else {
// 下一轮搜索区间是 [mid..right]
left = mid;
}
}
return left;
}
文本左右对齐算法
68题
最后一行要做特殊处理,将所有单词压缩在左侧,空格全部放在右侧,最后一行的单词之间最多只能有一个空格
findRight函数给定一个left作为起始单词,找到最多的符合maxWidth的单词下标
fillWord函数给定一个left和right索引,向这些单词中间填充空格来达到刚好maxWidth的长度,算法中先算出一共需要添加的空格数量,由于每个单词的末尾需要带一个空格(我们默认每个单词后都带了一个空格),但是最后一个单词的末尾不需要空格,对于maxWidth为10,拥有两个单词,第一个单词长度为2,第二个单词长度为3,它需要添加的空格数量为10+1-2-2-3=4;由于该行只有两个单词,它的间隙数spaceAVG=1,所有空格都填充在中间;
以上只是一个简单的例子,该题最离谱的要求在于尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧的空格数。也就是说如果有4个单词,三个间隙,要存放10个空格的话它只能是4,3,3这样存放,可以这样做10/3=3--1,我们每次计算当前的位置的索引与left的差值如果该差值小于余数就让他增加一个空格;其实这样做事很有道理的,如果一旦有余数,那他的第一个空隙一定会增加一个空格,如果每个空隙都增加一个空格肯定会大于能增加的空格总数,所以使用下标与余数的关系来确定是否需要增加空格
List<String> resList=new ArrayList<>();
public List<String> fullJustify(String[] words, int maxWidth) {
int left=0,lenW=words.length;
while(left<lenW){
int right=findRight(words,maxWidth,left);
if(right==lenW-1){
resList.add(fillWords(words,maxWidth,left,right,true));
}else{
resList.add(fillWords(words,maxWidth,left,right,false));
}
left=right+1;
}
return resList;
}
public String fillWords(String[] words,int maxWidth,int left,int right,boolean isLastLine){
int wordNums=right-left+1;
// 除去每个单词尾部空格, + 1 是最后一个单词的尾部空格的特殊处理
int spaceCount=maxWidth+1-wordNums;
for (int i=left;i<=right;i++) {
spaceCount -= words[i].length(); // 除去所有单词的长度
}
int spaceSuffix=1; // 词尾空格
int spaceAvg= (wordNums==1)? 1:spaceCount/(wordNums-1); // 额外空格的平均值
int spaceExtra= (wordNums==1)? 0:spaceCount%(wordNums-1);// 额外空格的余数
//String ans="";
StringBuilder sb=new StringBuilder();
for (int i=left;i<right;i++) {
sb.append(words[i]);// 填入单词
if (isLastLine){ // 特殊处理最后一行
sb.append(" ");
continue;
}
int sum=spaceSuffix + spaceAvg+ ((i-left)<spaceExtra?1:0);
while(sum-->0){sb.append(" ");}// 根据计算结果补上空格
//fill_n(back_inserter(ans), spaceSuffix + spaceAvg + ((i - bg) < spaceExtra), ' ');
}
sb.append(words[right]);// 填入最后一个单词
int sum=maxWidth - sb.length();
while(sum-->0){ sb.append(" ");}// 补上这一行最后的空格
//fill_n(back_inserter(ans), maxWidth - ans.size(), ' ');
return sb.toString();
}
//找到当前行最右边的单词下标
public int findRight(String[] words,int maxWidth,int left){
int right=left+1;
int countWord=words[left].length();
while(right<words.length&&(countWord+words[right].length()+1)<=maxWidth){
countWord += words[right].length()+1;
right++;
}
return right-1;
}
求和与+1
二进制求和
67题
改题的整体思路是两个字符串不等,但是我们可以用0将其处理成相等的字符串比如11和01,然后就可以进行处理了
二进制运算的过程为,sum/2为进位,sum%2为当前位,sum每次取值的时候要加上上一轮的进位值,在最后的时候如果进位为1还需要增加一个1在前面
public String addBinary(String a, String b) {
StringBuilder ans = new StringBuilder();
//carry 进位
int ca = 0;
for(int i = a.length() - 1, j = b.length() - 1;i >= 0 || j >= 0; i--, j--) {
int sum = ca;
//用0补齐较短的那个 sum+=当前字符或者0
sum += i >= 0 ? a.charAt(i) - '0' : 0;
sum += j >= 0 ? b.charAt(j) - '0' : 0;
ans.append(sum % 2);//当前位
ca = sum / 2;//进位
}
ans.append(ca == 1 ? ca : "");
//得到的是反向的字符,因为我们是从最后一个开始处理的
return ans.reverse().toString();
}
加一
66题
题目很简单,重点是如何用简洁的代码写出来,其实要考虑的情况也就是末尾为9的情况;对于非999的情况,我们直接进位,一旦取模于10不等于0就可以直接返回结果了,而等于999的情况,最终肯定是1000,所以只需要在0号位置附上1就行了
public int[] plusOne(int[] digits) {
for (int i = digits.length - 1; i >= 0; i--) {
digits[i]++;
digits[i] = digits[i] % 10;
if (digits[i] != 0) return digits;
}
digits = new int[digits.length + 1];
digits[0] = 1;
return digits;
}
判断是否是有效数字
65题
先筛选出不会出现的情况,e不能出现两次,使用一个idx变量记录字符串中e的位置,当idx等于-1的时候,说明整个有效数字中没有e的存在,对于没有e存在的情况和有e存在idx左边的情况其实是一样的,可以有.,对于有e存在右边的情况则是必须为数字不可以有.
check函数只在start处识别+和-;当有.的时候如果mustInteger或者有第二个.存在就直接return false;最后只要字符串是一连串数字就给他过;思路很简单重点在于理解题意
public boolean isNumber(String s) {
int n = s.length();
char[] cs = s.toCharArray();
int idx = -1;
// e/E只能出现一次
for (int i = 0; i < n; i++) {
if (cs[i] == 'e' || cs[i] == 'E') {
if (idx == -1) idx = i;
else return false;
}
}
boolean ans = true;
if (idx != -1) {
ans &= check(cs, 0, idx - 1, false);
ans &= check(cs, idx + 1, n - 1, true);
} else {
ans &= check(cs, 0, n - 1, false);
}
return ans;
}
boolean check(char[] cs, int start, int end, boolean mustInteger) {
if (start > end) return false;
if (cs[start] == '+' || cs[start] == '-') start++;
boolean hasDot = false, hasNum = false;
for (int i = start; i <= end; i++) {
if (cs[i] == '.') {
if (mustInteger || hasDot) return false;
hasDot = true;
} else if (cs[i] >= '0' && cs[i] <= '9') {
hasNum = true;
} else {
return false;
}
}
return hasNum;
}
路径与dp
路径
62题
一道很经典的动态规划题,机器人只能向下或者向右走,所以当前状态只能由上一个状态的上方和左方来,定义dp[i][j]为到达i,j位置的方法次数
public int uniquePaths(int m, int n) {
//dp[i][j]表示到到(i,j)有几种方法
int[][] dp = new int[m][n];
//base
for (int i = 0; i <m ; i++) {
dp[i][0]=1;
}
for (int i = 0; i <n ; i++) {
dp[0][i]=1;
}
for (int i = 1; i < m; i++) {
for (int j = 1; j <n ; j++) {
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
有障碍的路径
63题
需要对当前格子进行判断,如果当前格有障碍物那么到达它的机会==0,并且它如果在最左列或者最上行还会阻塞所有其他路径
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
if (obstacleGrid == null || obstacleGrid.length == 0) {
return 0;
}
// 定义 dp 数组并初始化第 1 行和第 1 列。
int m = obstacleGrid.length, n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
for (int i = 0; i < m ; i++) {
if(obstacleGrid[i][0]==1)break;
dp[i][0] = 1;
}
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
if(obstacleGrid[0][j]==1)break;
dp[0][j] = 1;
}
// 根据状态转移方程 dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 进行递推。
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 0) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 1];
}
螺旋矩阵
按照螺旋矩阵方式返回list
54题
其算法核心在于使用四个变量分别表示上下左右边界,在抵达矩阵的边界后缩小其对应边界,例如在第一行遍历完之后应该缩小上边界,所以up应该++;其循环的退出条件为上下边界错开或者左右边界错开的时候
List<Integer> res = new LinkedList<>();
if (matrix.length == 0) {
return res;
}
int up = 0, down = matrix.length - 1, left = 0, right = matrix[0].length - 1;
while (true) {
//输入第一排,第一排输入完毕后数字就没用了,直接舍弃掉
for (int col = left; col <= right; col++) {
res.add(matrix[up][col]);
}
//增加上边界
if (++up > down) break;
for (int row = up; row <= down; row++) {
res.add(matrix[row][right]);
}
//减少右边界
if (--right < left) break;
for (int col = right; col >= left; col--) {
res.add(matrix[down][col]);
}
//减少下边界
if (--down < up) break;
for (int row = down; row >= up; row--) {
res.add(matrix[row][left]);
}
//增加左边界
if (++left > right) break;
}
return res;
构建旋转矩阵
59题
与上题的思路基本一致
public int[][] generateMatrix(int n) {
int up=0;
int down=n-1;
int left=0;
int right=n-1;
int[][] res = new int[n][n];
int num=0;
while (true){
for (int i = left; i <= right; i++) {
res[up][i]=++num;
}
//上边界抵达下边界
if(++up>down)break;
for (int i = up; i <=down ; i++) {
res[i][right]=++num;
}
//右边界抵达左边界
if(--right<left)break;
for (int i = right; i >=left ; i--) {
res[down][i]=++num;
}
//下边界抵达上边界
if(--down<up)break;
for (int i = down; i >=up ; i--) {
res[i][left]=++num;
}
//左边界抵达右边界
if(++left>right)break;
}
return res;
}
插入区间
- 先一直添加区间:interval中的区间的右端点小于新区间的左端点,这是不重合的部分
- 随后判断interval中区间的左端点小于等于新区间右端点的位置,这代表他们是重合的,一直合并成新的区间,并且随后添加区间
- 最后当interval中的区间左端点大于新区间右端点的时候,继续添加集合,这也是不重合的部分
在ArrayList<int[]>转数组的时候使用如下方式写:res.toArray(new int[0][])可以提升程序运行速度
public int[][] insert(int[][] intervals, int[] newInterval) {
ArrayList<int[]> res = new ArrayList<>();
int len = intervals.length;
int i = 0;
// 判断左边不重合
while (i < len && intervals[i][1] < newInterval[0]) {
res.add(intervals[i]);
i++;
}
// 判断重合
while (i < len && intervals[i][0] <= newInterval[1]) {
newInterval[0] = Math.min(intervals[i][0], newInterval[0]);
newInterval[1] = Math.max(intervals[i][1], newInterval[1]);
i++;
}
res.add(newInterval);
// 判断右边不重合
while (i < len && intervals[i][0] > newInterval[1]) {
res.add(intervals[i]);
i++;
}
//list.toArray(new int[0][])这种写法确实有性能上的提升
return res.toArray(new int[0][]);
}
计算n次幂
50题
遇到关乎数字的题目要敏感边界问题,我们用long来取到n的值防止越界问题
此题的算法名为快速幂,其本质为分治算法,当N为奇数的时候多乘一个x即可,来看一个x的77次方的例子
x→x2→x4→+x9→+x19→x38→+x77一共经历了7次运算
public double myPow(double x, int n) {
long N = n;//防止-n操作引起的整形越界
return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
}
//快速幂方法如果要计算x的16次方,x-》x2-》x4-》x8-》x16 只要计算四次就行了而不是把x乘以15次
public double quickMul(double x, long N) {
if (N == 0) {
return 1.0;
}
double y = quickMul(x, N / 2);
//如果除以2处不尽就多加一个x
return N % 2 == 0 ? y * y : y * y * x;
}
由于递归需要额外的栈空间,所以尝试用迭代来做,当遇到奇数的时候直接把值赋给结果(事实上递归也是这么做的),然后其他的就按照不断的累乘就行了
public double myPow(double x, int n) {
long N = n;
return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
}
public double quickMul(double x, long N) {
double ans = 1.0;
// 贡献的初始值为 x
double x_contribute = x;
// 在对 N 进行二进制拆分的同时计算答案
while (N > 0) {
if (N % 2 == 1) {
// 如果 N 二进制表示的最低位为 1,那么需要计入贡献
ans *= x_contribute;
}
// 将贡献不断地平方
x_contribute *= x_contribute;
// 舍弃 N 二进制表示的最低位,这样我们每次只要判断最低位即可
N /= 2;
}
return ans;
}