数组
88. 合并两个有序数组
最简单的方法就是创建一个新数组,用空间换时间。将nums1和nums2的小的元素放入新数组中,最后再将新数组的值还给nums1.时间复杂度是O(n+m).但也多出来额外的空间。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i=0, j=0, k;
int[] res = new int[m+n];
for(k=0; i<m&&j<n; k++){
if(nums1[i]<=nums2[j]){
res[k] = nums1[i++];
}else{
res[k] = nums2[j++];
}
}
// 处理剩下的元素,剩下的元素一定大于当前res中的值。
//有点像归并排序
while(i<m){
res[k++] = nums1[i++];
}
while(j<n){
res[k++] = nums2[j++];
}
for(i=0; i<m+n; i++){
nums1[i] = res[i];
}
}
}
由于上面会出现额外的空间损耗,所以对代码进行改进。下面的代码是不会出现额外空间的。
思路就是将大的先放到nums1的末尾,初始的指针指在nums1和nums2的最后一位(即m-1和n-1),然后从大到小,从后往前填充nums1数组。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i=m-1,j=n-1,ptr=(m+n-1);
while(i>=0&&j>=0){
if(nums1[i]>=nums2[j]){
nums1[ptr--] = nums1[i--];
}else{
nums1[ptr--] = nums2[j--];
}
}
while(j>=0){
nums1[ptr--] = nums2[j--];
}
}
}
27. 移除元素
使用双指针,如果i不是val就后移,不需要处理;如果i==val,则i,j互换,同时j前移,将换出去的内容放在后面。j还前移的原因是因为如果不前移,且nums[i]和nums[j]如果是相同的值,则会一直死循环。
双指针往往就是处理数组,对数组进行更改(换位置),删除等。或者链表的操作。
双指针的初始位置有很多种,可以一开始在一起,或者一前一后,或者在初始位置和末尾。
class Solution {
public int removeElement(int[] nums, int val) {
int i=0, j=nums.length-1;
while(i<=j){
if(nums[i]==val){
int tmp = nums[i];
nums[i] = nums[j];
nums[j--] = tmp;
}else{
i++;
}
}
return i;
}
26. 删除有序数组中的重复项
实际上就是使用双指针,用fast进行指向的元素和前一个元素进行比较,如果相同,则只移动fast。反之,将fast指向的值赋给slow。最后返回slow。
class Solution {
public int removeDuplicates(int[] nums) {
int len = nums.length;
if(len==0){
return 0;
}
int fast=1,slow=1;
while(fast<len){
if(nums[fast]!=nums[fast-1]){
nums[slow++] = nums[fast];
}
fast++;
}
return slow;
}
}
双指针
125. 验证回文串
直接去除字符串中的不必要内容,再转为小写,最后用双指针比较即可。
class Solution {
public boolean isPalindrome(String s) {
String tmp = s.replaceAll("[^a-zA-Z0-9]", "").toLowerCase();
int left=0, right=tmp.length()-1;
while(left<right){
char c1=tmp.charAt(left), c2=tmp.charAt(right);
if(c1!=c2){
return false;
}else{
left++;
right--;
}
}
return true;
}
}
392. 判断子序列
双指针分别指向s和t,逐字对比,如果相同则指针同时向后,如果不同则只有t的指针后移。最终如果s的指针大小等同于s的长度即为true。也就是s的最后一个字符也得到了匹配,导致指针的大小为s的长度。
class Solution {
public boolean isSubsequence(String s, String t) {
int i=0,j=0;
int lens=s.length(), lent=t.length();
while(i<lens&&j<lent){
char c1=s.charAt(i), c2=t.charAt(j);
if(c1!=c2){
j++;
}else{
i++;
j++;
}
}
return i==lens;
}
}
167. 两数之和 II - 输入有序数组
题目描述
双指针应用于处理数组,如果这里的双指针是固定一个元素,然后向后遍历元素,寻找和为target的结果,那就是低效率的,也不是双指针,只是两重循环罢了,没用到非递减序列的条件。
如果要应用非递减序列,可以把双指针放在一前一后的位置,如果和大于target,则后面的指针前移,反之前面的指针后移。直到找到和为target为止。
class Solution {
public int[] twoSum(int[] numbers, int target) {
int[] res = new int[2];
int i=0, j=numbers.length-1;
while(i<j){
int tmp=numbers[i]+numbers[j];
if(tmp==target){
res[0]=i+1;
res[1]=j+1;
break;
}else if(tmp>target){
j--;
}else{
i++;
}
}
return res;
}
}
11. 盛最多水的容器
题目描述
实质上是贪心+双指针,具体的思路在注释里了。
class Solution {
public int maxArea(int[] height) {
int max=0,s,len;// s是容积,len是底边长
int i=0, j=height.length-1;
// 面积是最短边×两边间隔
// 从两边进行遍历 每次只移动短的边
// 因为移动长边,无论如何,面积一定会变小(不过长边移动后是变大还是变小)
// 移动短边,则可能会变大,因此移动短边才有意义。
while(i<j){
len=j-i;
if(height[i]<height[j]){ // 左边是短边
s=height[i]*len;
i++;
}else{ // 右边是短边
s=height[j]*len;
j--;
}
max=Math.max(max,s);
}
return max;
}
}
滑动窗口
209. 长度最小的子数组
用滑动窗口,如果窗口内值大于target则左边向右移动,小于target则右边右移动。
当sum恰好等于target时,我们需要计算当前子数组的长度,并尝试将其与之前找到的最短长度进行比较,更新res。
当sum大于target时,我们需要缩小窗口,直到子数组的和小于target。在这个过程中,每次sum大于或等于target时,我们都可能得到一个新的、更短的满足条件的子数组长度,因此我们需要在每次缩小窗口时都更新res。
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int left = 0, right = 0;
int sum = 0, len = nums.length;
int res = Integer.MAX_VALUE;
while (right < len) {
sum += nums[right++];
while (sum >= target) {
res = Math.min(res, right - left);
sum -= nums[left++];
}
}
return res == Integer.MAX_VALUE ? 0 : res;
}
}
3. 无重复字符的最长子串
滑动窗口的模板大概就是两个循环,外面的循环控制右边界,内部的循环控制左边界。
class Solution {
public int lengthOfLongestSubstring(String s) {
int i=0,j=0,len=s.length();
int res=0,cur=0;
if(len==0){
return 0;
}
for(; j<len; j++){
cur+=1;
for(int k=i; k<j; k++){// 去重
if(s.charAt(k)==s.charAt(j)){
i=k+1;
cur=(j-k);
break;
}
}
res=Math.max(cur,res);
}
return res;
}
}
哈希表
383. 赎金信
首先可以按照HashMap的方式来写,但这样其实效率并不高:
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
Map<Character, Integer> map = new HashMap<>();
// 获得map
for(int i=0; i<magazine.length(); i++){
char key = magazine.charAt(i);
if(map.containsKey(key)){
map.put(key,map.get(key)+1);
}else{
map.put(key,1);
}
}
// 填充ransomnote
for(int i=0; i<ransomNote.length(); i++){
char key = ransomNote.charAt(i);
if(map.containsKey(key)&&map.get(key)!=0){
map.put(key,map.get(key)-1);
}else{
return false;
}
}
return true;
}
}
可以改为数组的写法,因为题目里的内容都是小写字母,因此可以直接用数组来表示。在网站跑出的结果有显著提高。
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
int[] res = new int[26];
Arrays.fill(res,0);
int len1=ransomNote.length(), len2=magazine.length();
for(int i=0; i<len2; i++){
char c=magazine.charAt(i);
res[c-'a']+=1;
}
for(int i=0; i<len1; i++){
char c=ransomNote.charAt(i);
res[c-'a']-=1;
if(res[c-'a']<0){
return false;
}
}
return true;
}
}
205. 同构字符串
题目描述
不能单纯的查看键存在了,值是否冲突,还要查看有值的时候,是否存在对应的键。例如s=bad,t=bab,这种情况b对应b,a对应a,但d对应b这种情况只看键是否存在看不出来,需要看值存在,键是否存在。
class Solution {
public boolean isIsomorphic(String s, String t) {
HashMap<Character, Character> map = new HashMap<>();
int len=t.length();
for(int i=0; i<len; i++){
char c1=s.charAt(i), c2=t.charAt(i);
if(map.containsKey(c1)){ // 检查是否已经包含对应关系
if(c2!=map.get(c1)){ // 有对应关系,但出现冲突
return false;
}
}else{
if (map.containsValue(c2)) { // 有值,但不存在对应的键
return false;
}
map.put(c1,c2);
}
}
return true;
}
}
290. 单词规律
题目描述
和上一题类似,不过需要判断一下长度是否相同,且字符串可以用split改为字符串数组,会简单很多。
class Solution {
public boolean wordPattern(String pattern, String s) {
String[] words = s.split(" ");
int len=pattern.length();
HashMap<Character, String> map = new HashMap<>();
if(len!=words.length){
return false;
}
for(int i=0; i<len; i++){
char c=pattern.charAt(i);
if(map.containsKey(c)){
if(!map.get(c).equals(words[i])){
return false;
}
}else{
if(map.containsValue(words[i])){
return false;
}
map.put(c, words[i]);
}
}
return true;
}
}
栈
20. 有效的括号
题目描述
经典括号序,一一对应即可。也可以用HashMap,左右括号分别为键和值。
public class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
int len=s.length();
for(int i=0; i<len; i++){
char c=s.charAt(i);
if(c=='('||c=='['||c=='{'){
stack.push(c);
}else{
if(stack.isEmpty()){
return false;
}
char top = stack.pop();
if((c==')'&&top!='(')||
(c==']'&&top!='[')||
(c=='}'&&top!='{')){
return false;
}
}
}
return stack.isEmpty();
}
}
71. 简化路径
题目链接
这题的思路仍然是栈。唯一说困难的地方就是对字符串的分割和拼接。
分割字符串仍然用split(),像我一样用正则也可以,如果不用正则也可以在for里判断是否为”“而去掉docs中的空白内容。
拼接使用join,join可以将数组的内容用特定字符拼接,就像代码里写的。
class Solution {
public String simplifyPath(String path) {
Stack<String> stack = new Stack<>();
String[] docs = path.substring(1).split("/+");
int len=docs.length;
for (String s : docs) {
if (s.equals("..")) {
if (!stack.isEmpty()) {
stack.pop();
}
} else if (!s.equals("") && !s.equals(".")) {
stack.push(s);
}
}
String res = "/" + String.join("/", stack);
return res;
}
}
155. 最小栈
题目链接
没有太多难度(如果不考虑性能),主要能够按照面向对象的思想写出类即可。
用到两个栈,mainStack和普通栈无异,可以模仿普通stack的pop,push,top操作。
minStack则记录最小值,其中的内容是越小的值越趋于栈顶。因此minStack的push操作需要设计,在push的值大于minstack的栈顶元素时,就不需要push。确保可以在常数时间找到最小值。而pop操作也一样,如果pop的值和minStack栈顶元素一样,则minstack也要pop,为了确保最小值能被正确更新。
class MinStack {
private Stack<Integer> mainStack;
private Stack<Integer> minStack;
public MinStack() {
mainStack = new Stack<>();
minStack = new Stack<>();
}
public void push(int val) {
mainStack.push(val);
if(minStack.isEmpty()||minStack.peek()>=val){
minStack.push(val);
}
}
public void pop() {
int n=mainStack.pop();
if(minStack.peek()==n){
minStack.pop();
}
}
public int top() {
return mainStack.peek();
}
public int getMin() {
return minStack.peek();
}
}
链表
141. 环形链表
题目描述
实际上就是快慢指针(双指针)的应用。
初始化都为head,且快指针和慢指针的移动都要一开始就进行,否则会一直出现true(因为一开始都在head)。若初始化为head和head.next,则在判断条件上会更麻烦。因此直接初始化为head,判断条件为快指针以及快指针的next不为空即可。
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow=head,fast=head;
while(fast != null && fast.next != null){
slow=slow.next;
fast=fast.next.next;
if(fast==slow){
return true;
}
}
return false;
}
}
21. 合并两个有序链表
题目描述
引入哑节点 dummy 来简化链表的合并操作,最终返回 dummy.next 作为结果链表的头节点。
合并过程:
使用 while(p1 != null && p2 != null) 循环遍历两个链表,按顺序将较小的节点连接到结果链表中。
每次将较小的节点连接到 current.next,并将 current 移动到下一个节点。
处理剩余的节点:循环结束后,可能还剩下未遍历完的链表部分,直接将其连接到结果链表的尾部。
返回结果:最终返回 dummy.next,它指向合并后的链表的头节点。
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 创建一个dummy节点
ListNode dummy = new ListNode(0);
// 使用current指针来构建新的链表
ListNode current = dummy;
// 遍历两个链表,按顺序合并
while (list1 != null && list2 != null) {
if (list1.val <= list2.val) {
current.next = list1;
list1 = list1.next;
} else {
current.next = list2;
list2 = list2.next;
}
current = current.next;
}
// 如果有一个链表还有剩余,直接连接到新链表的末尾
if (list1 != null) {
current.next = list1;
} else {
current.next = list2;
}
// 返回合并后的链表头节点
return dummy.next; // 跳过dummy节点,返回实际的头节点
}
}