目录
#前言
目前在学习数据结构和准备算法比赛,想再次记录一些经典或者有意思的简单题目,步步深入供自己复习。学习路径是跟着代码随想录学习的,所以有些思路和代码源于"代码随想录",感谢"代码随想录”的无私开源,让我们这些不知从何开始学习算法的小白有一条明路,谢谢~
数组
题目一:移除元素(思想:快慢指针)
解法一: 快慢指针
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
两种方法:
1.一快一慢
2.一左一右
快慢指针法
class Solution { public int removeElement(int[] nums, int val) { // 定义快慢指针 // 慢指针用来更新数组的元素,快指针来找是否符合条件的元素 // 比如[3,2,2,3] nums[0] == val不满足条件所以跳过,nums[slowIndex]先不改 // nums[1] != val 符合条件,nums[slowIndex] = nums[fastIndex] 即 nums[0] = nums[1] , slowIndex慢指针索引++ // 最终结果为[2,2,2,3] => [2,2] int slowIndex = 0; for(int fastIndex = 0 ; fastIndex < nums.length ; fastIndex++){ if(nums[fastIndex] != val){ nums[slowIndex] = nums[fastIndex]; slowIndex++; } } return slowIndex; } }
相向双指针法
定义左右双指针,left指针用来搜索是否有符合条件的值,right指针负责提供一个值替换left指针符合条件的情况,然后right--,即右指针左移。
但是right指针有可能也等于val,所以left指针不急着自增,再在下面加一个else条件,如果此时换过来的值确实不等于val再自增,如果等于val就换right--的值。
//学习到的相向双指针法 class Solution { public int removeElement(int[] nums, int val) { int left = 0; int right = nums.length - 1; while(left <= right){ if(nums[left] == val){ nums[left] = nums[right]; right--; }else{ left++; } } return left; } }
题目二:有序数组的平方(思想:排序,双指针)
解法一:暴力解法
直接平方后调用Arrays.sort()库函数排序,很简单就不在此演示了。
解法二:双指针
还是双指针中的左右指针方法,定义左右指针,再新建个数组和index记录最大值
由题目可知数组已经是排序好的,只是平方后大小可能会发生变化。
但是我们可以知道的是平方后数组的最大值要不就是最左边要不就是最右边,所以我们再判断此时的最左和最右哪个大就将哪个放到新数组的末尾即可。然后index自减,左边最大就左边再自增,右边最大就右边自减继续判断下一个。
class Solution { public int[] sortedSquares(int[] nums) { int left = 0; int right = nums.length - 1; int[] res = new int[nums.length]; int index = nums.length - 1; while(left <= right){ if(nums[left] * nums[left] > nums[right] * nums[right]){ res[index] = nums[left] * nums[left]; index--; left++; }else{ res[index] = nums[right] * nums[right]; index--; right--; } } return res; } }
以上的思路都比较好理解,不过我第一次的else方法写错了导致超时,因为没考虑到最左等于最大的情况。
总结:看到数组的题目可以先考虑到双指针思想,感觉还是出现的比较频繁的。
其次就是排序的各种方法还是需要去学习一下。
题目三:长度最小的子数组(思想:移动滑块)
在解这道题之前,学习了一下滑动窗口。
滑动窗口是算法的四大思想之一。算法四大思想:滑动窗口,贪心,回溯,动态规划。
概念:在上面的双指针我们说到双指针有“快慢指针”和“相向指针”两种。滑动窗口也可以认为是一种双指针,是“快慢指针”的特例。
滑动窗口。窗口是两个变量left和right之间的元素,也就是一个区间。窗口的大小有两中场景。
- 固定窗口。即窗口的大小是确定的,一般先确定窗口是否越界,再进行逻辑处理,如求窗口中元素的最大,最小或平均值。
- 可变窗口。一般判断是否满足要求,再进行逻辑处理。一般求数组中的最大,最小窗口大小。(本题就是可变窗口)
// 移动窗口+贪心 class Solution { public int minSubArrayLen(int target, int[] nums) { // 定义慢指针 int slowIndex = 0; // 定义结果为无穷大,最后进行判断,如果result还是等于无穷大那就是没找到,返回0。 int result = Integer.MAX_VALUE; int sum = 0; for(int fastIndex = 0 ; fastIndex < nums.length ; fastIndex++){ // 快指针累加到sum sum += nums[fastIndex]; // 这里是最需要主要的,是用的while循环,要不断比较子序列是否符合条件 while(sum >= target){ // 每次都取最小,(fastIndex - slowIndex + 1)即子序列的长度 result = Math.min(result , fastIndex - slowIndex + 1); // sum减去子序列头的值,为下面的子序列位置自增做准备 sum -= nums[slowIndex]; // 不断改变子序列的起始位置 slowIndex++; } } return result == Integer.MAX_VALUE?0:result; } }
题目四:螺旋矩阵(思想:模拟)
class Solution { public int[][] generateMatrix(int n) { // 矩阵定义 int[][] nums = new int[n][n]; // 每一层循环x,y的起始值 // 其实x和y可以定义为一个变量,因为每次循环x和y都是自增1(向右下对角线移动) int start_x = 0; int start_y = 0; // 当前层数,从0开始 int layer = 0; // 对应(i,j),每一层时对该层行,列数的处理 int i = 0; int j = 0; // 偏移量,其实也就是层数,再定义多一次方便理解 int offset = 1; // 给螺旋矩阵赋值 int count = 1; // 循环的次数,也就是层数 while(layer < n/2){ // 处理上边 for(j = start_y; j < n - offset; j++){ nums[start_x][j] = count++; } // 处理右边 for(i = start_x; i < n - offset; i++){ nums[i][j] = count++; } // 上面两个for语句结束后,i和j仍会自增,所以此时i和j均等于n-offset,所以i和j在下面不用再赋值了 // 处理下边 for(; j > start_x; j--){ nums[i][j] = count++; } // 处理左边 for(; i > start_y ; i--){ nums[i][j] = count++; } // start的x和y同时向右下角移动一个位置,即往内进一层 start_x++; start_y++; // 偏移加1,层数增加一层 offset++; layer++; } // 最后的判断,如果n % 2 == 1即n是奇数,最中心再插入一个值 if(n % 2 == 1){ nums[start_x][start_y] = count++; } return nums; } }
以后再写循环或此类题目的时候,要保持“循环不变量"的原则。
这里是采用左闭右开的原则。
每个步骤已经详细的写在注释中了,再自己画图理解一下。
链表
学习数据结构的时候是学的c++版,由于参赛的Java组,所以还是得记录一下Java链表的用法。
方法一:直接调用库函数
// 引入 LinkedList 类
import java.util.LinkedList;
LinkedList<E> list = new LinkedList<E>();
常用的方法:
list.add(E element) / (int index, E element) 分别为在链尾添加元素和在指定位置添加元素
list.addFirst(E e) / addLast(E e) 元素添加到链头和链尾
clear() 清空链表
removeFirst() / removeLast() 删除并返回第一个/最后一个元素
get(int index) / getFirst() / getLast() 获得元素
size() 返回元素个数
方法二:定义链表
public class ListNode {
// 结点的值
int val;
// 下一个结点
ListNode next;
// 节点的构造函数(无参)
public ListNode() {
}
// 节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}
// 节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
除此之外,还学习了一个链表问题:虚拟节点(dummy)
简单来说,我们在写代码前可以先定义一个虚拟节点,这个节点的作用是操作可能会涉及到头节点。每次用链表前写上即可,公式化的东西。
// 方法如下 // -1可以是任意一个不正常的值,表示这个节点是dummy ListNode* dummy=new ListNode(-1); dummy->next=head; // 最后返回 return dummy->next;
题目一:移除链表元素
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode removeElements(ListNode head, int val) { // 为空直接返回 if(head == null)return head; // 设置dummy节点 ListNode dummyHead = new ListNode(-1); dummyHead.next = head; ListNode temp = dummyHead; while(temp.next != null){ if(temp.next.val == val){ temp.next = temp.next.next; }else{ temp = temp.next; } } return dummyHead.next; } }
题目二:翻转链表(思想:双指针,栈)
解法一:
利用栈结构进行翻转
先把每个遍历到的节点的val值压栈,再每个弹栈给新建的链表,主要是创建链表那需要注意。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode reverseList(ListNode head) { // temp指针(指向head以便接下来的移动) ListNode temp = head; if(temp == null)return temp; // 定义栈,实现翻转 Stack<Integer> stack = new Stack<>(); // 指针temp指向null时结束 while(temp != null){ // 把每个遍历到的节点的值压栈 stack.push(temp.val); // 移动指针 temp = temp.next; } // 创建链表 ListNode nodeSta = new ListNode(0); ListNode nextNode; nextNode = nodeSta; while(stack.size() != 0){ ListNode node = new ListNode(stack.pop()); nextNode.next = node; nextNode = nextNode.next; } // nodeSta节点为0 所以下一个节点才是创建的链表 nextNode = nodeSta.next; return nextNode; } }
解法二:
双指针解法
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode reverseList(ListNode head) { ListNode cur = head; ListNode temp = null; ListNode prev = null; while(cur != null){ temp = cur.next; cur.next = prev; prev = cur; cur = temp; } return prev; } }
题目三:删除链表的倒数第N个节点(思想:双指针,暴力)
解法一:
暴力
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummyNode = new ListNode(-1); dummyNode.next = head; ListNode temp = dummyHead; // 记录头节点 ListNode nodeSta = temp; // 空情况 if(temp == null)return temp; // 只有一个节点且n=1的情况 if(temp.next == null && n == 1)return temp.next; int count = 0; // 记录链表长度 while(temp != null){ temp = temp.next; count++; } // 因为上面结束后temp已经到链尾了,所以要复位一下 temp = nodeSta; // count=n的情况(主要是这里要注意) if(count == n)return temp.next; // 接下来就是移动指针 for(int i = 1; i <= count - n; i++){ if(i == count - n){ temp.next = temp.next.next; } temp = temp.next; } temp = nodeSta; return temp; } }
解法二:
双指针
这种寻值的可以第一时间想到双指针
But,链表的题目如果设计到增/删的还是要new一个dummy节点较好
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummyNode = new ListNode(-1); dummyNode.next = head; ListNode slowNode = dummyNode; ListNode fastNode = dummyNode; // 只要快慢指针相差 n 个结点即可 for(int i = 0; i <= n; i++){ fastNode = fastNode.next; } while(fastNode != null){ fastNode = fastNode.next; slowNode = slowNode.next; } //此时 slowIndex 的位置就是待删除元素的前一个位置。 slowNode.next = slowNode.next.next; return dummyNode.next; } }
题目四:两两交换链表中的的节点(思想:虚拟节点)
题解:
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode swapPairs(ListNode head) { ListNode dummyHead = new ListNode(-1); dummyHead.next = head; ListNode cur = dummyHead; ListNode firstNode; ListNode secondNode; ListNode temp; // 存在first和seconde节点 while(cur.next != null && cur.next.next != null ){ firstNode = cur.next; secondNode = cur.next.next; temp = cur.next.next.next; cur.next = secondNode; secondNode.next = firstNode; firstNode.next = temp; cur = firstNode; } return dummyHead.next; } }
这道题主要是利用dummy节点链接second节点,方便操作
哈希表
题目一:有效的字母异位词(思想:哈希表的value自增,char字符串排序)
方法一:哈希表
class Solution { public boolean isAnagram(String s, String t) { if(s.length() != t.length())return false; Map<Character,Integer> map = new HashMap<>(); // 添值 for(int i = 0; i < s.length(); i++){ int addCount = map.containsKey(s.charAt(i)) ? map.get(s.charAt(i)) : 0; map.put(s.charAt(i), addCount+1); } // 减值 for(int i = 0; i < t.length(); i++){ if(!map.containsKey(t.charAt(i)))return false; int subCount = map.get(t.charAt(i)); map.put(t.charAt(i),subCount-1); } // 验证 for(int i = 0; i < s.length(); i++){ if(map.get(s.charAt(i)) != 0)return false; } return true; } }
主要就是先吧字符串s的字母添加进去,再遍历字符串t的字母并将对应的value减一
最后再验证即可
主要是value自增的方法
int addCount = map.containsKey(s.charAt(i)) ? map.get(s.charAt(i)) : 0; map.put(s.charAt(i), addCount+1);
方法二:字符串排序
class Solution { public boolean isAnagram(String s, String t) { if(s.length() != t.length())return false; char[] str1 = s.toCharArray(); char[] str2 = t.toCharArray(); Arrays.sort(str1); Arrays.sort(str2); return Arrays.equals(str1,str2); } }
可以把字符串先转为char类型的数组( toCharArray() ),再进行排序,比较