一 基础学习
1. 数组
1. 两数之和
public class TwoSum {
/**
* 1. 两数之和
*/
public int[] twoSum1(int[] nums, int target) {
/*
* 暴力破解
*/
int[] arr = new int[2];
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if(nums[i]+nums[j] == target){
arr[0] = i;
arr[1] = j;
}
}
}
return arr;
}
public int[] twoSum2(int[] nums, int target) {
/*
* 哈希表:一次循环,判断target-nums[i]是否在字典中,
* 如果在,则返回对应下标,如果不在则将nums[i]作为键,下标i作为值存入字典中。
*/
Map<Integer, Integer> hashmap = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; i++) {
if(hashmap.containsKey(target - nums[i])){
return new int[] {hashmap.get(target - nums[i]),i};
}
hashmap.put(nums[i], i);
}
return new int[2];
}
}
2. 最接近的三数之和
public class ThreeSumClosest {
/**
* 16. 最接近的三数之和
*/
public int threeSumClosest1(int[] nums, int target) {
/*
* 暴力破解
*/
int sum = 0;
int error = 10001;
for (int i = 0; i < nums.length; i++) {
for (int i1 = i + 1; i1 < nums.length; i1++) {
for (int i2 = i1 + 1; i2 < nums.length; i2++) {
if (Math.abs(target - (nums[i] + nums[i1] + nums[i2])) < error) {
sum = nums[i] + nums[i1] + nums[i2];
error = Math.abs(target - sum);
}
}
}
}
return sum;
}
public int threeSumClosest2(int[] nums, int target) {
/*
* 排序+双指针
* 遍历一遍数组,将nums[1]作为第一个加数,双指针p、q初始化分别指向num[i+1]与nums[nums.length-1]
* 定义result为最终结果,当目标值与三数之和的差值 小于 目标值与result的差值时,更新result;
* 当三数之和大于目标值时,那么将q往小的调,即将q往左移;当三数之和小于目标值时,那么将p往大的调,即将p往右移;
* 当p == q时,表示将nums[i]作为第一个加数的所有情况遍历完了。
*/
Arrays.sort(nums);
int result = nums[0] + nums[1] + nums[2];
for (int i = 0; i < nums.length; i++) {
int p = i + 1;
int q = nums.length - 1;
while (p < q) {
int sum = nums[i] + nums[p] + nums[q];
if (Math.abs(target - sum) < Math.abs(target - result)) {
result = sum;
}
if (sum < target) {
p++;
} else if (sum > target) {
q--;
} else {
return result;
}
}
}
return result;
}
}
3. 删除有序数组中的重复项
public class RemoveDuplicates {
/**
* 26. 删除有序数组中的重复项
*/
public int removeDuplicates(int[] nums) {
/*
* 双指针;
* 定义p、q指针,初始化分别指向nums[0],nums[1],前提是数组长度大于等于2
* 若 nums[p] == nums[q] 那么 q++;
* 若 nums[p] != nums[q] 那么 p++,nums[p] = nums[q],q++;
* 当 p 指向最后一个值完后,终止,返回q;
*/
// 边界判断
if (nums.length < 2) {
return nums.length;
}
int p = 0;
int q = 1;
for (; q < nums.length; q++) {
if(nums[p] == nums[q]){
continue;
}else {
p++;
nums[p] = nums[q];
}
}
return p+1;
}
}
4. 移除元素
public class RemoveElement {
/**
* 27. 移除元素
*/
public int removeElement1(int[] nums, int val) {
/*
* 双指针;只是移除元素,所以只需要遍历一次数组,当nums[i]与val相等时,那么改变nums[i]为后面与val不等的值就好了。
* 初始化指针p、q分别指向nums[0]、nums[0];
* 当nums[q] == val 时,q++;
* 当nums[q] != val 时,将nums[p] = nums[q]; p++,q++;
*/
// 边界判断
if (nums.length == 0) {
return 0;
}
int p = 0;
for (int q = 0; q < nums.length; q++) {
if(nums[q] != val){
nums[p] = nums[q];
p++;
}
}
return p;
}
}
5. 三数之和
public class ThreeSum {
/**
* 15. 三数之和
*/
private List<List<Integer>> result;
public List<List<Integer>> threeSum(int[] nums) {
/*
* 排序+双指针;与 "16.最接近的三数之和" 可以用同样的方法
* 先对数组排序;然后遍历一遍数组;
* 双指针p、q分别初始化指向 i+1 与 nums.length-1
* 判断 nums[i] + nums[q] + nums[q]
* 若 = 0,则添加进列表中,p++、q--
* 若 < 0,则p++;若 > 0,则q--;当p == q时,跳出循环
* 因为要求三个元素不能重复,所以当 nums[i] == nums[i-1]时,continue;
* nums[p] == nums[p-1]时,continue
* nums[q] == nums[q+1]时,continue
*/
result = new ArrayList<List<Integer>>();
// 边界判断
if (nums.length < 3) {
return result;
}
// 数组排序
Arrays.sort(nums);
for (int i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int p = i + 1;
int q = nums.length - 1;
while (p < q) {
if (p > i + 1 && nums[p] == nums[p - 1]) {
p++;
continue;
}
if (q < nums.length - 1 && nums[q] == nums[q + 1]) {
q--;
continue;
}
if ((nums[i] + nums[p] + nums[q]) < 0) {
p++;
} else if ((nums[i] + nums[p] + nums[q]) > 0) {
q--;
} else {
// 创建一个列表,临时存储三数之和为0的值
List<Integer> tempList = new ArrayList<Integer>();
tempList.add(nums[i]);
tempList.add(nums[p]);
tempList.add(nums[q]);
result.add(tempList);
p++;
q--;
}
}
}
return result;
}
}
6. 单词规律
public class WordPattern {
/**
* 290. 单词规律
*/
public boolean wordPattern(String pattern, String s) {
/*
* 哈希表;
* 先将s按照' '分割成单词String数组s_list,然后判断数组长度与pattern数组长度是否相同,若不同,返回false;
* 创建一个map集合,一遍遍历,将pattern[i]作为集合的key,s_list[i]作为集合的value
* 如果当前pattern[i]不在集合的key中,且集合的value中也没有当前的s_list[i](判断这个是为了避免出现一值对应多键的情况),
* 那么将pattern[i]、s_list[i]作为键值对插入集合中,
* 如果当前pattern[i]在集合的key中,那么判断当前的s_list[i]与集合中pattern[i]这个键对应的值是否相同,若不同,返回false;
* 遍历完成后,返回true;
*/
String[] s_list = s.split(" ");
// 判断分割后的长度s_list与pattern长度是否相同
if (s_list.length != pattern.length()) {
return false;
}
// 创建一个map,其中建存储pattern的字符,值存储s_list的字符串
Map<Character, String> hashmap = new HashMap<Character, String>();
char[] patternList = pattern.toCharArray();
for (int i = 0; i < patternList.length; i++) {
// 如果集合中不包含当前的键和值那么进行添加
if (!hashmap.containsKey(patternList[i])) {
// 若出现一个值对应多个键,返回false
if (hashmap.containsValue(s_list[i])) {
return false;
}
hashmap.put(patternList[i], s_list[i]);
} else {
String value = hashmap.get(patternList[i]);
if (!value.equals(s_list[i])) {
return false;
}
}
}
return true;
}
}
2. 链表
1. 链表定义
public class ListNode {
int val;
ListNode next;
ListNode() {
}
ListNode(int val) {
this.val = val;
}
ListNode(ListNode next){
this.next = next;
}
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
2. 移除链表元素
public class RemoveElements {
/**
* 203. 移除链表元素
*/
/**
* 非递归
*/
public ListNode removeElements1(ListNode head, int val) {
/*
* 只需要遍历一遍链表,当遇到节点值与val相同的节点时,删除该节点
* 注意:先要设置一个头结点(这个头结点没有值),而且不可以直接用头节点去.next,那样会导致原链表发生变化。
*/
// 给当前链表加上一个哑节点,因为链表的头节点随时都可能被删除。
ListNode temp = new ListNode(head);
// 将头节点传给pre
ListNode pre = temp;
// 遍历链表
while (pre.next != null) {
if (pre.next.val == val) {
pre.next = pre.next.next;
} else {
pre = pre.next;
}
}
return head.next;
}
/**
* 递归法
*/
public ListNode removeElements2(ListNode head, int val) {
/*
* 对头结点之后的链表进行删除,取出头节点,如果头结点的值等于val,则删除头结点,即head = head.next;
* 否则head = head.next进行上述判断之后的结果。
* 接下来操作剩下的链表,和上述步骤一样;直到head为空时,终止;
*/
if (head == null) {
return head;
}
head.next = removeElements2(head.next, val);
return head.val == val ? head.next : head;
}
}
3. 合并两个有序链表
public class MergeTwoLists {
/**
* 21. 合并两个有序链表
* 其中递归法有点像从尾结点开始往左合并,迭代法相当于从头结点开始往右一个一个的合并。
*/
/**
* 递归法
*/
public ListNode mergeTwoLists1(ListNode list1, ListNode list2) {
/*
* 当list1[0]较小时,为 list1[0] + mergeTwoLists1(list1[1:], list2)
* list2[0]较小时,同理。
* 需要考虑的边界情况
* 当list1或者list2为空链表时,就直接返回另一个不为空的链表
* 当有一个链表为空时,递归结束
*/
if (list1 == null) {
return list2;
} else if (list2 == null) {
return list1;
} else if (list1.val < list2.val) {
list1.next = mergeTwoLists1(list1.next, list2);
return list1;
} else {
list2.next = mergeTwoLists1(list1, list2.next);
return list2;
}
}
/**
* 非递归
*/
public ListNode mergeTwoLists2(ListNode list1, ListNode list2) {
/*
* 设置一个哨兵节点:哨兵,顾名思义,是用来解决边界问题的,
* 在许多算法中,存在“邻居依赖问题”,在处理当前元素时,要涉及到它旁边那个元素。
* 那如果当前元素是边界元素呢,它没有旁边那个元素,如果不作处理,程序就可能出错;
* 如果对它特别对待,就会增加代码复杂性,还会降低程序效率。
* 应用哨兵,也就是申请若干个多余的元素作为边界元素的邻居,可以完美得解决这个问题。
*
* 比如在链表中:单链表在插入和删除时,需要修改前驱结点的后继指针,这就形成了“邻居依赖”,
* 链表中第一个元素没有前驱结点,如果没有特殊处理,在插入第一个结点时,就会出错。
* 所以我们可以申请一个头结点,作为原本的第一个结点的前驱结点,问题也就解决了。
*
* 所以首先定义一个哨兵节点,其实也就是头节点head,然后判断 list1 与 list2 的值谁小,
* head.next 指向小的那个链表,然后小的链表后移一位,
* 当其中一个链表为空时,把另一个链表剩下的部分再插到后面就好了
*
*/
// 创建一个头结点
ListNode head = new ListNode(0);
// 头结点复制一份传给当前节点curNode,防止原始链表被改变
ListNode curNode = head;
// 循环判断插入 当有一个为空时跳出循环
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
curNode.next = list1;
// 节点值小的链表后移一位
list1 = list1.next;
} else {
curNode.next = list2;
list2 = list2.next;
}
// 当前节点后移一位,指向下一个节点;当list1 或者list2其中一个为空时,curNode.next == null
curNode = curNode.next;
}
// 判断哪一个链表不为空,将不为空的链表在接到curNode的后面;
curNode.next = list1 == null ? list2 : list1;
return head.next;
}
}
4. 相交链表
public class GetIntersectionNode {
/**
* 160. 相交链表
*/
/**
* 暴力破解
*/
public ListNode getIntersectionNode1(ListNode headA, ListNode headB) {
/*
* 暴力破解法,遍历headA,headA的每个节点都和headB进行比对
*/
// 边界条件
if (headA == null || headB == null) {
return null;
}
ListNode curNodeA = headA;
ListNode curNodeB = headB;
while (curNodeA != null) {
while (curNodeB != null) {
if (curNodeA == curNodeB) {
return curNodeA;
}
curNodeB = curNodeB.next;
}
curNodeA = curNodeA.next;
// 将curNodeB节点又重新指向headB
curNodeB = headB;
}
return new ListNode(0);
}
/**
* 利用栈
*/
public ListNode getIntersectionNode2(ListNode headA, ListNode headB) {
/*
* 定义两个当前节点curNodeA与curNodeB分别指向headA与headB的头结点
* 要判断两个链表的后面是否有共同节点,那么可以从后往前判断,
* 两个链表都从尾结点开始如果相同那么继续往前走,直到出现不相同的节点,那最后一个相同的节点就是第一个
* 但是链表不支持从尾向头查,所以可以利用栈
*/
// 边界处理
if (headA == null || headB == null) {
return null;
}
// 定义两个指针分别指向两个链表的头节点
ListNode curNodeA = headA;
ListNode curNodeB = headB;
// 初始化两个栈
Stack<ListNode> stackA = new Stack<ListNode>();
Stack<ListNode> stackB = new Stack<ListNode>();
// 将两个链表压入栈
while (curNodeA != null) {
stackA.push(curNodeA);
curNodeA = curNodeA.next;
}
while (curNodeB != null) {
stackB.push(curNodeB);
curNodeB = curNodeB.next;
}
// 定义一个节点表示返回值
ListNode result = null;
// 出栈判断,当其中一个栈为空或者两个栈的值不相等时,结束循环
while (!stackA.empty() && !stackB.empty() && stackA.peek().equals(stackB.peek())) {
result = stackA.pop();
stackB.pop();
}
return result;
}
/**
* 双指针
*/
public ListNode getIntersectionNode3(ListNode headA, ListNode headB) {
/*
* 定义两个当前节点curNodeA与curNodeB分别指向headA与headB的头结点
* 分别计算两个链表的长度,然后对长链表的开始位置与短链表的长度相同,这样一起往下判断就好了
*/
// 边界处理
if (headA == null || headB == null) {
return null;
}
// 定义两个变量计算链表的长度
int listALength = 0;
int listBLength = 0;
// 定义两个指针分别指向两个链表的头节点
ListNode curNodeA = headA;
ListNode curNodeB = headB;
// 计算两个链表的长度
while (curNodeA != null) {
curNodeA = curNodeA.next;
listALength++;
}
while (curNodeB != null) {
curNodeB = curNodeB.next;
listBLength++;
}
// 定义长链表要先往下遍历的次数
int number = Math.abs(listALength - listBLength);
// 定义长短链表的头指针
ListNode shortList = listALength < listBLength ? headA : headB;
ListNode longList = listALength < listBLength ? headB : headA;
// 长链表先移动到后面的节点长度与短链表的长度相同的节点位置
while (number > 0) {
longList = longList.next;
number--;
}
// 循环判断,当两个节点不同时,继续往下遍历,相同时,结束循环
while (longList != shortList) {
longList = longList.next;
shortList = shortList.next;
}
// 若没有相同节点时,longList通过上面的循环已经变成空了,所以可以直接返回longList
return longList;
}
/**
* 利用哈希表
*/
public ListNode getIntersectionNode4(ListNode headA, ListNode headB) {
/*
* 将链表A先存在哈希表中,然后遍历链表B,判断链表B的每个节点是否在链表A中,
* 若有一个在,那后面的那些一定都在,则直接返回这个节点
* 若没有,返回null
*/
Set<ListNode> nodeSet = new HashSet<ListNode>();
// 定义一个指针指向链表A的头节点
ListNode curNode = headA;
// 将链表A的节点存入set中
while (curNode != null) {
nodeSet.add(curNode);
curNode = curNode.next;
}
// 当链表A存完之后 curNode = null,所以直接将 curNode 设为 headB
curNode = headB;
// 遍历链表B判断是否在set中
while (curNode != null) {
if (nodeSet.contains(curNode)) {
return curNode;
}
curNode = curNode.next;
}
return curNode;
}
/**
* 双指针进阶
*/
public ListNode getIntersectionNode5(ListNode headA, ListNode headB) {
/*
* 定义两个指针curNodeA、curNodeB分别指向headA、headB,同时更新两个指针
* 当curNodeA指针为空时,将curNodeA指向headB,不为空时,指向下一个节点;curNodeB同理
* 当curNodeA、curNodeB两个指针指向的节点相同或都为空时,结束循环。返回它们指向的节点或null。
*
* 证明:
* 假设两个链表长度分别为m、n,相交节点之前的长度分别为a、b,相交的长度为c,那么有 a+c=m,b+c=n
* 两个链表相交:
* 若 a==b,两个链表相交节点之前的长度一样,则遍历到相交节点那将终止;
* 若 a!=b,指针curNodeA遍历节点长度 a+c+b,指针curNodeB遍历节点长度 b+c+a,会遇到相同节点,返回该节点。
* 两个链表不相交:
* 若 m==n,两个指针遍历到最后都指向null,返回null;
* 若 m!=n,指针curNodeA遍历的节点长度为 m+n,指针curNodeB遍历的节点长度为 a+c+b,最终都指向null,返回null
*/
// 边界处理
if (headA == null || headB == null) {
return null;
}
// 定义两个指针分别指向两个链表
ListNode curNodeA = headA;
ListNode curNodeB = headB;
while (curNodeA != curNodeB) {
curNodeA = curNodeA == null ? headB : curNodeA.next;
curNodeB = curNodeB == null ? headA : curNodeB.next;
}
return curNodeA;
}
/**
* 利用环,找环的入口节点
*/
public ListNode getIntersectionNode6(ListNode headA, ListNode headB) {
return null;
}
}
5. 删除排序链表中的重复元素 II
public class DeleteDuplicates {
/**
* 82. 删除排序链表中的重复元素 II
*/
public ListNode deleteDuplicates1(ListNode head) {
/*
* 哨兵节点
* 定义两个指针dummyCopy、curNode分别指向哨兵节点与头结点,
* 如果curNode.val == curNode.next.val(此处要注意curNode与curNode.next不能为空)
* 那么记录下这个值为num,curNode继续往下移动,直到curNode的值不等于num了,(向下移动判断的时候,curNode也不能为空)
* 将这一段重复的节点在dummyCopy中删除,怎样删除?dummyCopy的下一个节点直接指向值不等于num的节点
* 若不等于,则将dummyCopy节点就等于curNode节点,然后curNode节点向下一个节点移动。
*/
// 边界处理
if (head == null) {
return null;
}
// 定义一个哨兵节点,值为-101是因为链表值得范围为[-100,100]
ListNode dummy = new ListNode(-101, head);
// 将头节点复制一份传给curNode
ListNode dummyCopy = dummy;
// 定义当前节点指向head
ListNode curNode = head;
while (curNode != null && curNode.next != null) {
if (curNode.val == curNode.next.val) {
int num = curNode.val;
while (curNode != null && curNode.val == num) {
curNode = curNode.next;
}
// 此处很关键,是相当于把dummyCopy断开了,将它里面值等于num的节点删除了,它的下一个节点直接指向了值不等于num的节点
dummyCopy.next = curNode;
} else {
/*
* 此处写dumpyCopy = curNode,而非dummyCopy.next = curNode,
* 是因为下面curNode指向了下一个节点,dummyCopy同样也就指向了下一个节点
*/
dummyCopy = curNode;
curNode = curNode.next;
}
}
return dummy.next;
}
}
6. 旋转链表
public class RotateRight {
/**
* 61. 旋转链表
*/
public ListNode rotateRight1(ListNode head, int k) {
/*
* 链表右移k个长度;
* 首先算出链表的长度length,然后将 k % length 得到一个向右移动的次数;
* 然后再将链表的首尾相连,再在从尾往右数 k % length 个节点的地方断开就好了
* 也就是说从头往尾数的第 (length - 1) - k % length 处的节点为尾结点
*/
//边界判断
if (head == null) {
return head;
}
// 将头节点复制一份传给tempHead,放置在操作头结点的过程中使原链表发生变化
ListNode tempHead = head;
// 计算链表长度
int linkedListLength = 0;
/*
* 当while循环结束时,tempHead 又变为了null,
* 因为要首尾相连,即tempHead应该取到最后一个节点,然后让最后一个节点的next=head
* 所以不可以用tempHead != null进行判断,这样会使tempHead最终变为null
* 应该使用tempHead.next != null进行判断;
* 当tempHead.next == null时,tempHead正好为尾节点,这样就可以直接在后面加head了
* 这样会使计算的链表长度少1个节点,长度最后再加1
*/
while (tempHead.next != null) {
linkedListLength++;
tempHead = tempHead.next;
}
/*
* 因为链表不是双向的,所以不好从尾部往上数k % linkedListLength个节点,
* 所以从头往尾数(linkedListLength - 1) - k % linkedListLength个节点找到尾节点
*/
linkedListLength++;
k = (linkedListLength - 1) - k % linkedListLength;
// 使链表变成循环链表:尾节点的下一个节点为头结点
tempHead.next = head;
// 从左往右数(linkedListLength - 1) - k % linkedListLength个节点,找到要断开的地方,但是是从尾节点开始数的,所以长度不需要减一
k++;
while (k > 0) {
k--;
tempHead = tempHead.next;
}
head = tempHead.next;
tempHead.next = null;
return head;
}
public ListNode rotateRight2(ListNode head, int k) {
/*
* 优化后的代码
*/
// 边界判断,当k==0或链表长度为1时,都不用移动
if (head == null || k == 0 || head.next == null) {
return head;
}
// 初始化链表长度,初始化为1,这样后面遍历到尾节点之后就不用再加一了
int linkedListLength = 1;
// 将头节点复制一份传给tempHead
ListNode tempHead = head;
while (tempHead.next != null){
linkedListLength++;
tempHead = tempHead.next;
}
// 因为是从尾节点开始数的,所以需要加一,而要找到尾节点又需要减一,所以不加不减
k = linkedListLength - k % linkedListLength;
// 当 k % linkedListLength == 0 时,说明移动后与移动前一样
if (k % linkedListLength == 0){
return head;
}
// 首尾相连
tempHead.next = head;
// 找尾节点
while (k>0){
k--;
tempHead = tempHead.next;
}
// 头结点为的尾节点的下一个节点
head = tempHead.next;
// 尾节点之后为空
tempHead.next = null;
return head;
}
}
3. 栈
1. 有效的括号
public class IsValid {
/**
* 20. 有效的括号
*/
public boolean isValid(String s) {
/*
* 用栈判断,一遍遍历
* 如果当前的字符为括号的左半边 "({[" ,那么就入栈,
* 如果是括号的右半部分,那么就弹出栈顶元素,如果配对,那么继续,如果不配对,那么直接返回false
* 当遍历完成后,栈应该为空,不为空则证明也有不配对的。
* 当字符串的长度为奇数时,直接返回false。
*/
// 字符串转字符数组
char[] charArray = s.toCharArray();
if (charArray.length % 2 != 0) {
return false;
}
// 创建一个栈,用来存括号的左半部分
Stack<Character> stack = new Stack<Character>();
for (int i = 0; i < charArray.length; i++) {
if (charArray[i] == '(' || charArray[i] == '{' || charArray[i] == '[') {
stack.push(charArray[i]);
} else {
if (charArray[i] == ')' && !stack.empty()) {
if (!stack.pop().equals('(')) {
return false;
}
} else if (charArray[i] == '}' && !stack.empty()) {
if (!stack.pop().equals('{')) {
return false;
}
} else if (charArray[i] == ']' && !stack.empty()) {
if (!stack.pop().equals('[')) {
return false;
}
} else {
return false;
}
}
}
return stack.empty();
}
}
2. 逆波兰表达式求值
public class EvalRPN {
/**
* 150. 逆波兰表达式求值
*/
public int evalRPN(String[] tokens) {
/*
* 利用栈。遍历一遍数组,如果是数字那么就入栈,如果是符号就弹出栈中的两个元素进行运算,运算后的值在入栈,
* 最后返回栈顶元素,栈内也就剩下一个值了。
* 注意:再进行计算的时候,应为 后出栈的数(+-*除)先出栈的数
* 我不理解,在本地写tokens[i] == "*" 这种就可以运行,在力扣上就会抛java.lang.NumberFormatException
*/
// 创建一个栈
Stack<Integer> stack = new Stack();
for (int i = 0; i < tokens.length; i++) {
String token = tokens[i];
if (isNumber(token)) {
stack.push(Integer.parseInt(tokens[i]));
} else {
int num2 = stack.pop();
int num1 = stack.pop();
if ("*".equals(token)) {
stack.push(num1 * num2);
} else if ("/".equals(token)) {
stack.push(num1 / num2);
} else if ("+".equals(token)) {
stack.push(num1 + num2);
} else if ("-".equals(token)) {
stack.push(num1 - num2);
}
}
}
return stack.pop();
}
public boolean isNumber(String token) {
return !("+".equals(token) || "-".equals(token) || "*".equals(token) || "/".equals(token));
}
}
3. 最小栈
/**
* 155. 最小栈
*/
public class MinStack {
/*
* 用一个辅助栈来存储最小值,当每次插入一个值时,辅助栈也插入一个当前最小值与该值比较之后的较小值
* 弹出时,辅助栈也弹出元素,最终可以直接返回辅助栈的最小值。
*/
// 创建成员属性
private Deque<Integer> stack;
private Deque<Integer> minStack;
public MinStack() {
// 创建一个链表用来表示栈
stack = new LinkedList<Integer>();
minStack = new LinkedList<Integer>();
// 最小栈中添加一个最大值,方便后面比较
minStack.add(Integer.MAX_VALUE);
}
public void push(int val) {
stack.push(val);
minStack.push(Math.min(val, minStack.peek()));
}
public void pop() {
/*
* 此处删除元素时应确保栈不为空。
*/
stack.pop();
minStack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
4. 比较含退格的字符串
public class BackspaceCompare {
/**
* 844. 比较含退格的字符串
*/
public boolean backspaceCompare1(String s, String t) {
/*
* 将两个字符串分别压入两个栈,再压入的时候进行判断,如果当前的字符时#,那么不进栈,且当栈不为空时再往外弹一个,进行退格。
* 入栈完成后将两个栈的元素出栈比对,一样就继续,不一样立马返回false,有一个栈为空则跳出循环。
* 最后两个栈都为空则,返回true,否则false
*/
// 创建两个栈
Stack sStack = new Stack();
Stack tStack = new Stack();
// 将两个字符串压入栈
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (ch == '#') {
if (!sStack.empty()) {
sStack.pop();
}
} else {
sStack.push(ch);
}
}
for (int i = 0; i < t.length(); i++) {
char ch = t.charAt(i);
if (ch == '#') {
if (!tStack.empty()) {
tStack.pop();
}
} else {
tStack.push(ch);
}
}
while (!sStack.empty() && !tStack.empty()) {
if (sStack.peek().equals(tStack.peek())) {
sStack.pop();
tStack.pop();
} else {
return false;
}
}
return sStack.empty() && tStack.empty() ? true : false;
}
/**
* 双指针
*/
public boolean backspaceCompare2(String s, String t) {
/*
* 定义两个指针p、q分别指向s、t,指针用来记录当前待删除字符的数量,初始化为0;
* 从后面往前面遍历,如果当前元素是'#'号时,指针++,
* 如果当前值不是'#',在判断指针为不为0,
* 若为0,则固定当前元素
* 若不为0,则指针继续往左移,
* 为false的情况:两个字符不相等或者一个字符都遍历完了,另一个还没完。
* 当两个字符都遍历完成后,跳出循环
*/
// 获取两个字符串的长度
int sLength = s.length() - 1;
int tLength = t.length() - 1;
// 定义两个指针
int p = 0;
int q = 0;
// 当两个字符串长度都为0时,跳出循环
while (sLength >= 0 || tLength >= 0) {
// 指针指向当前
while (sLength >= 0) {
if (s.charAt(sLength) == '#') {
p++;
sLength--;
} else if (p > 0) {
sLength--;
p--;
} else {
break;
}
}
while (tLength >= 0) {
if (t.charAt(tLength) == '#') {
q++;
tLength--;
} else if (q > 0) {
tLength--;
q--;
} else {
break;
}
}
if (sLength >= 0 && tLength >= 0) {
// 若字符串不相等,返回false
if (s.charAt(sLength) != t.charAt(tLength)) {
return false;
}
} else {
// 若一个字符当前长度为正,一个字符当前长度为负,返回false
if (sLength >= 0 || tLength >= 0) {
return false;
}
}
sLength--;
tLength--;
}
return true;
}
}
5. 基本计算器 II
public class Calculate {
/**
* 227. 基本计算器 II
*/
public int calculate(String s) {
/*
* 先计算乘除运算,没有括号,但是有空格;需要遍历一遍s
* 先定义个变量preSign来表示运算符,初始化为'+',因为式子中的数为整数,所以第一个数为=0+第一个数
* 有的整数可能是多位数,怎样获取字符串中的整数可以包含多位数?
* 定义一个数为num,初始化为0,num = 0 * num + s.charAt(i);
* 遇到一个运算符就进行一次判断,若为'+'num直接入栈,'-'num变成相反数直接入栈,若为'*'、'/'弹出栈顶元素与num计算后在入栈
* 当遇到下一个运算符或者i=n-1时,说明一个整数遍历完了,则将运算符更新为当前运算符,num = 0;
* 最后将栈里的元素进行相加在返回。
*/
Deque<Integer> stack = new LinkedList<Integer>();
char perSign = '+';
int num = 0;
int n = s.length();
for (int i = 0; i < n; i++) {
// 将一个多位整数提取出来
if (Character.isDigit(s.charAt(i))) {
num = 0 * num + s.charAt(i) - '0';
}
if (!Character.isDigit(s.charAt(i)) && s.charAt(i) != ' ' || i == n - 1) {
switch (perSign) {
case '+':
stack.push(num);
break;
case '-':
stack.push(-num);
break;
case '*':
stack.push(stack.pop() * num);
break;
default:
stack.push(stack.pop() / num);
break;
}
perSign = s.charAt(i);
num = 0;
}
}
int result = 0;
while (!stack.isEmpty()) {
result = result + stack.pop();
}
return result;
}
}
4. 字符串
1. 验证回文串
public class IsPalindrome {
/**
* 125. 验证回文串
*/
/**
* 创建了新变量
*/
public boolean isPalindrome1(String s) {
/*
* 双指针,定义p指向开头,定义指针q指向结尾,
* 如果当前指针指向的字符不是字母,那么就向左或向右移动,如果两个都指向的是字符,那么进行一个比对,不一样则返回false,否则则继续,最终返回true。
* 当两个指针碰面时则跳出while循环,而且不考虑字母大小写
*/
String str = "";
// 先对字符串s进行清洗
for (int i = 0; i < s.length(); i++) {
if (Character.isLetterOrDigit(s.charAt(i))) {
str += s.charAt(i);
}
}
// 如果没有字符返回true
if (str.length() == 0) {
return true;
}
int p = 0;
int q = str.length() - 1;
while (p < q) {
if (Character.toLowerCase(str.charAt(p)) != Character.toLowerCase(str.charAt(q))) {
return false;
} else {
p++;
q--;
}
}
return true;
}
/**
* 原地操作
*/
public boolean isPalindrome(String s) {
/*
* 双指针,定义p指向开头,定义指针q指向结尾,
* 如果当前指针指向的字符不是字母,那么就向左或向右移动,如果两个都指向的是字符,那么进行一个比对,不一样则返回false,否则则继续,最终返回true。
* 当两个指针碰面时则跳出while循环,而且不考虑字母大小写
*/
int p = 0;
int q = s.length() - 1;
while (p < q) {
// 此处需要加一句左指针要小于右指针
while (p < q && !Character.isLetterOrDigit(s.charAt(p))) {
p++;
}
while (q > p && !Character.isLetterOrDigit(s.charAt(q))) {
q--;
}
if (Character.toLowerCase(s.charAt(p)) != Character.toLowerCase(s.charAt(q))) {
return false;
} else {
p++;
q--;
}
}
return true;
}
}
2. 验证回文字符串 Ⅱ
public class ValidPalindrome {
/**
* 680. 验证回文字符串 Ⅱ
*/
/**
* 暴力破解,超出时间限制
*/
public boolean validPalindrome1(String s) {
/*
* 暴力破解:先写一个验证字符串是不是回文串的函数,不删除字符进行判断,遍历一遍s删除第i个字符判断,true则返回,false继续,最终返回false
*/
// 边界判断
if (s.length() < 3) {
return true;
}
if (isPalindrome(s)) {
return true;
}
for (int i = 0; i < s.length(); i++) {
if (isPalindrome(remove(s, i))) {
return true;
}
}
return false;
}
public boolean isPalindrome(String s) {
/*
* 判断字符串是不是回文串,双指针,相同则同时往左和右移动,不同则返回,当两个指针相遇时,结束
*/
int p = 0;
int q = s.length() - 1;
while (p < q) {
if (s.charAt(p) != s.charAt(q)) {
return false;
}
p++;
q--;
}
return true;
}
public String remove(String s, int index) {
/*
* 删除第index个字符
*/
if (s.length() <= index) {
return s;
}
String str = "";
for (int i = 0; i < s.length(); i++) {
if (i == index) {
continue;
} else {
str += s.charAt(i);
}
}
return str;
}
/**
* 贪心
*/
public boolean validPalindrome2(String s) {
/*
* 双指针
* 定义两个指针left,right;分别初始化为 0 与 s.length()-1
* 当 s[left] == s[right] 时,left++,right--
* 当 s[left] != s[right] 时,判断 s[left+1,right]与s[left,right-1]是不是回文串,如果是,返回true,否则false;
*/
//边界处理
if (s.length() < 3) {
return true;
}
int left = 0;
int right = s.length() - 1;
while (left < right) {
if (s.charAt(left) == s.charAt(right)) {
left++;
right--;
} else {
return isPalindrome(s, left + 1, right) || isPalindrome(s, left, right - 1);
}
}
return true;
}
private boolean isPalindrome(String s, int left, int right) {
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
}
3. 字符串解码
public class DecodeString {
// 为一个栈的方法定义一个成员变量
public int i;
/**
* 394. 字符串解码
*/
public String decodeString1(String s) {
/*
* 创建两个栈,一个栈存放字符串,一个栈存放数字
* 遍历字符串s,用两个变量记录字符串,和数字
* 若当前s.charAt(i)为字符,则累加,当到最后一个且最后一个字符不为']'时,将字符串在入栈。
* 当遇到数字,且字符串不为空时,字符串入栈
* 当遇到'['时,数字入栈
* 当遇到']'时,括号里的字符串入栈,然后遍历字符串栈,将弹出来的字符串按照先出来的在后面的关系拼接在一起,
* 直到遇到'['时表示一个括号内的的字符串全拼接在一起了,然后进行累加,并入栈
* 最后出栈。
*/
// 建立两个栈,一个栈存放数字,一个栈存放字符串
Deque<String> strStack = new LinkedList<String>();
Deque<String> numStack = new LinkedList<String>();
// 创建一个变量缓存字符串
String tempStr = "";
// 创建一个变量计数
String numStr = "";
for (int i = 0; i < s.length(); i++) {
if (Character.isLetter(s.charAt(i))) {
tempStr += s.charAt(i);
if (i == s.length() - 1 && s.charAt(i) != ']') {
strStack.push(tempStr);
tempStr = "";
}
} else if (Character.isDigit(s.charAt(i))) {
numStr += s.charAt(i);
if (!tempStr.equals("")) {
strStack.push(tempStr);
tempStr = "";
}
} else if (s.charAt(i) == '[') {
numStack.push(numStr);
numStr = "";
strStack.push("[");
} else {
strStack.push(tempStr);
tempStr = "";
String curStr = "";
while (!strStack.peek().equals("[")) {
curStr = strStack.pop() + curStr;
}
strStack.pop();
int num = Integer.parseInt(numStack.pop());
for (int i1 = 0; i1 < num; i1++) {
tempStr = tempStr + curStr;
}
strStack.push(tempStr);
tempStr = "";
}
}
// 出栈
String result = "";
while (!strStack.isEmpty()) {
result = strStack.pop() + result;
}
return result;
}
/**
* 一个栈
*/
public String decodeString2(String s) {
/*
* 如果是字符就拼接在一起进栈
* 如果是数字也拼接在一起进栈
* 如果是'[',单独进栈
* 如果是']'那么开始出栈,将出栈元素拼接在一起,直到遇到'[',弹出'[',此时栈顶元素必为数字
* 弹出栈顶数字,进行拼接,然后在入栈。
* 最后出栈拼接返回
*/
//创建一个栈
Deque<String> stack = new LinkedList<String>();
// 初始化变量指针i指向当前的的s[i]
i = 0;
while (i < s.length()) {
// 数字拼接在一起进栈
if (Character.isDigit(s.charAt(i))) {
//拼接数字
String numStr = getDigits(s);
// 数字入栈
stack.push(numStr);
} else if (Character.isLetter(s.charAt(i)) || s.charAt(i) == '[') {
// 当前字符为字符或者'['时,入栈
// 一个字符一个字符的进栈
stack.push(String.valueOf(s.charAt(i++)));
}else {
// 当前字符为']',开始出栈,拼接一个'[]'中的字符,然后按照对应数字累加
i++;
// 创建一个变量缓存当前字符
String tempStr = "";
while (!stack.peek().equals("[")){
tempStr = stack.pop() + tempStr;
}
// 左括号出栈
stack.pop();
// 弹出栈顶数字
int count = Integer.parseInt(stack.pop());
// 定义一个变量缓存累加后的结果
String curStr = "";
for (int i1 = 0; i1 < count; i1++) {
curStr += tempStr;
}
// 拼接后的字符串入栈
stack.push(curStr);
}
}
// 定义一个变量缓存结果
String result = "";
while (!stack.isEmpty()){
result = stack.pop() + result;
}
return result;
}
public String getDigits(String str) {
// 此处字符串缓冲区来存储数字
StringBuffer stringBuffer = new StringBuffer();
while (Character.isDigit(str.charAt(i))) {
stringBuffer.append(str.charAt(i++));
}
return stringBuffer.toString();
}
}
4. Excel表列名称
public class ConvertToTitle {
/**
* 168. Excel表列名称
*/
public String convertToTitle(int columnNumber) {
/*
* 不用建立字典,直接将数字+64转成char
* columnNumber % 26, 得到一个余数num,str = char(num+64) + str
* columnNumber // 26,得到一个商,如果商还大于26,那再 %,//;直到商小于26
*/
String str = "";
if (columnNumber <= 26) {
str = (char) (columnNumber + 64) + str;
}
while (columnNumber > 26) {
int remainder = columnNumber % 26;
columnNumber /= 26;
if (remainder == 0) {
str = (char) (26 + 64) + str;
} else {
str = (char) (remainder + 64) + str;
}
if (columnNumber <= 26) {
str = (char) (columnNumber + 64) + str;
}
}
return str;
}
}
5. 颠倒字符串中的单词
public class ReverseWords {
/**
* 151. 颠倒字符串中的单词
*/
/**
* 栈
*/
public String reverseWords1(String s) {
/*
* 用栈解决,遍历一遍s,将s添加到栈中
* 入栈:当前字符是" "时,继续,当前字符不为" "时,用一个tempStr记录当前字符,将其拼成一个完整的单词;
* 当前字符是字符且下一个字符是" "时,tempStr入栈,并置空,或者运行到s的末尾时,tempStr入栈。
* 出栈:用一个变量记录每次出栈的单词,当栈不为空时,在字符后面加一个空格,最后一个单词出栈后,就不用再加空格了。
*/
// 定义一个变量缓存单词
String tempStr = "";
Deque<String> stack = new LinkedList<String>();
// 入栈
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == ' ') {
continue;
}
// 当前字符不是数字或者字母时,
if(s.charAt(i) != ' '){
tempStr += s.charAt(i);
if((i+1 < s.length() && s.charAt(i+1) == ' ') || i == s.length() - 1){
stack.push(tempStr);
tempStr = "";
}
}
}
// 出栈
// 创建一个变量存储结果
String result = "";
// 出栈
while (!stack.isEmpty()) {
result += stack.pop();
if(!stack.isEmpty()){
result += " ";
}
}
return result;
}
/**
* 双端队列
*/
public String reverseWords2(String s) {
/*
* 将字符串一个字符一个字符的入队,每次入队前判断当前的队尾是否是空格,和当前元素是否是空格;
* 出队时去掉队头的空格和队尾的空格
*/
Deque<Character> deque = new LinkedList<Character>();
deque.addFirst(s.charAt(0));
for (int i = 1; i < s.length(); i++) {
if(deque.peekLast().equals(' ') && s.charAt(i) == ' '){
continue;
}else {
deque.addLast(s.charAt(i));
}
}
// 去除头部空格
while (!deque.isEmpty()){
if(deque.peekFirst() == ' '){
deque.removeFirst();
}else {
break;
}
}
// 去除尾部空格
while (!deque.isEmpty()){
if(deque.peekLast() == ' '){
deque.removeLast();
}else {
break;
}
}
String result = "";
String tempWord = "";
while (!deque.isEmpty()){
if(!deque.peekLast().equals(' ')){
tempWord = deque.removeLast() + tempWord;
}else {
result += tempWord;
result += deque.removeLast();
tempWord = "";
}
if(deque.isEmpty()){
result += tempWord;
}
}
return result;
}
/**
* 调用API
*/
public String reverseWords3(String s) {
/*
* 先将字符串分割成字符数组,按照空格分
* 然后在将字符数组进行反转
* 最后将反转后的字符数组拼接
*/
// 除去开头与结尾的空格
s = s.trim();
// 按照长空格正则表达式将字符串切分为字符列表
List<String> sList = Arrays.asList(s.split("\\s+"));
// 列表反转
Collections.reverse(sList);
return String.join(" ",sList);
}
}
2 初阶学习
1. 树
1. 二叉树的最大深度
public class MaxDepth {
/**
* 104. 二叉树的最大深度
*/
/**
* DFS递归
*/
public int maxDepth1(TreeNode root) {
/*
* 当节点为空时,返回0
* 不为空时返回 1 + 左右子树最大的深度
* 左右子树最大的深度:重复上面的过程
*/
if (root == null) {
return 0;
}
return 1 + Math.max(maxDepth1(root.left), maxDepth1(root.right));
}
/**
* DFS非递归
*/
public int maxDepth2(TreeNode root) {
/*
* 采用栈进行遍历,然后再加一个节点的层数栈作为辅助
* 每次出栈时,节点和节点层数一起出栈,然后 result = max(result, 节点层数)
* 节点的左右孩子入栈时,层数+1;
*/
int result = 0;
if (root == null) {
return result;
}
// 创建一个栈
Deque<TreeNode> nodeStack = new LinkedList<TreeNode>();
Deque<Integer> levelStack = new LinkedList<Integer>();
// 根节点入栈
nodeStack.push(root);
levelStack.push(1);
while (!nodeStack.isEmpty()) {
TreeNode curNode = nodeStack.pop();
int curLevel = levelStack.pop();
result = Math.max(result, curLevel);
if (curNode.right != null) {
nodeStack.push(curNode.right);
levelStack.push(curLevel+1);
}
if (curNode.left != null) {
nodeStack.push(curNode.left);
levelStack.push(curLevel+1);
}
}
return result;
}
/**
* BFS非递归
*/
public int maxDepth3(TreeNode root) {
/*
* 层序遍历
* 创建一个队列,每一层入队时,层数加一
*/
// 定义返回值
int result = 0;
if (root == null) {
return result;
}
// 创建一个队列
Deque<TreeNode> deque = new LinkedList<TreeNode>();
// 根节点入栈
deque.offer(root);
while (!deque.isEmpty()) {
// 获取每一层的节点个数
int size = deque.size();
// 当前层的节点出队,它的左右子节点入队
for (int i = 0; i < size; i++) {
TreeNode curNode = deque.poll();
if (curNode.left != null) {
deque.offer(curNode.left);
}
if (curNode.right != null) {
deque.offer(curNode.right);
}
}
result++;
}
return result;
}
}
2. 二叉树的最小深度
public class MinDepth {
/**
* 111. 二叉树的最小深度
*/
/**
* DFS递归
*/
public int minDepth1(TreeNode root) {
/*
* 难点:返回条件如何确定
*/
// 1. 根节点为空,返回0
if(root == null){
return 0;
}
// 2. 有一个子树为空,返回另一个子树的最小深度,又因为子树为空,返回 0,所以二者相加
int deepLeft = minDepth2(root.left);
int deepRight = minDepth2(root.right);
if(root.left == null || root.right == null){
return deepLeft + deepRight + 1;
}
// 3. 两个子树都不为空,那么就返回最小的就好了
return 1 + Math.min(deepLeft, deepRight);
}
/**
* DFS迭代: 先序遍历
*/
public int minDepth2(TreeNode root) {
/*
* 用一个节点栈和一个节点层数栈来实现,和最大路径一样
* 但是判断最短路径的的方法变了,只有当前节点是叶子节点时才进行判断。
*/
int result = 10000000;
if (root == null) {
return 0;
}
Deque<TreeNode> nodesStack = new LinkedList<TreeNode>();
Deque<Integer> levelStack = new LinkedList<Integer>();
// 根节点入栈
nodesStack.push(root);
levelStack.push(1);
while (!nodesStack.isEmpty()) {
TreeNode curNode = nodesStack.pop();
int curLevel = levelStack.pop();
if (curNode.right != null) {
nodesStack.push(curNode.right);
levelStack.push(curLevel + 1);
}
if (curNode.left != null) {
nodesStack.push(curNode.left);
levelStack.push(curLevel + 1);
}
// 只有当前节点是叶子节点时,才要进行对比,获得最小深度
if (curNode.left == null && curNode.right == null) {
result = Math.min(result, curLevel);
}
}
return result;
}
/**
* BFS迭代
*/
public int minDepth3(TreeNode root) {
/*
* BFS就简单了,不需要全部遍历完,只需要找到第一个出现的叶子节点就好了,它所在的那一层就是最小深度
* 循环的地方加入一个标记符号,表示找没找到第一个叶子节点。
*/
int result = 0;
if (root == null) {
return result;
}
// 创建一个队列
Deque<TreeNode> deque = new LinkedList<TreeNode>();
// 根节点入队
deque.offer(root);
// 定义返回结果
// 定义一个布尔变量来表示有没有遇到叶子节点,没遇到,则为true
boolean symbol = true;
// 当队列不为空,且没有出现叶子节点时,继续循环
while (!deque.isEmpty() && symbol) {
result++;
int size = deque.size();
for (int i = 0; i < size; i++) {
TreeNode curNode = deque.poll();
if (curNode.right == null && curNode.left == null) {
symbol = false;
break;
}
if (curNode.left != null) {
deque.offer(curNode.left);
}
if (curNode.right != null) {
deque.offer(curNode.right);
}
}
}
return result;
}
}
3. 路径总和
public class HasPathSum {
/**
* 112. 路径总和
*/
/**
* DFS递归:前序遍历
*/
public boolean hasPathSum1(TreeNode root, int targetSum) {
/*
* 根节点为空,返回false;
* 从左到右,遇到叶子节点判断它的值与剩余的值一样不一样。一样返回true,否则:false
*/
if (root == null) {
return false;
}
if (root.left == null && root.right == null) {
return root.val == targetSum;
}
return hasPathSum1(root.left, targetSum - root.val) || hasPathSum1(root.right, targetSum - root.val);
}
/**
* DFS迭代
*/
public boolean hasPathSum2(TreeNode root, int targetSum) {
/*
* 先序遍历
* 一个节点栈,一个节点的值栈,
* 若遇到叶子节点,判断叶子节点的值与目标值是否一样,一放就返回true
*/
if (root == null){
return false;
}
// 节点栈
Deque<TreeNode> nodesStack = new LinkedList<TreeNode>();
// 节点的值栈
Deque<Integer> nodesValueStack = new LinkedList<Integer>();
// 入栈
nodesStack.push(root);
nodesValueStack.push(targetSum);
while (!nodesStack.isEmpty()){
TreeNode curNode = nodesStack.pop();
int curNodeValue = nodesValueStack.pop();
// 遇到叶子节点的判断
if(curNode.left == null && curNode.right == null && curNode.val == curNodeValue){
return true;
}
// 入栈
if(curNode.right != null){
nodesStack.push(curNode.right);
nodesValueStack.push(curNodeValue - curNode.val);
}
if(curNode.left != null){
nodesStack.push(curNode.left);
nodesValueStack.push(curNodeValue - curNode.val);
}
}
return false;
}
/**
* BFS迭代
*/
public boolean hasPathSum3(TreeNode root, int targetSum) {
/*
* 与DFS迭代一样,两个队列,一个存储节点,一个存储节点对应的目标值
*/
if(root == null){
return false;
}
// 节点队列
Deque<TreeNode> nodeQueue = new LinkedList<TreeNode>();
// 节点值队列
Deque<Integer> nodeValueQueue = new LinkedList<Integer>();
// 入队
nodeQueue.offer(root);
nodeValueQueue.offer(targetSum);
while (!nodeQueue.isEmpty()){
// 节点队列长度
int size = nodeQueue.size();
for (int i = 0; i < size; i++) {
// 节点出队
TreeNode curNode = nodeQueue.poll();
int curNodeVaule = nodeValueQueue.poll();
if(curNode.left == null && curNode.right == null && curNode.val == curNodeVaule){
return true;
}
// 入队
if(curNode.left != null){
nodeQueue.offer(curNode.left);
nodeValueQueue.offer(curNodeVaule - curNode.val);
}
if(curNode.right != null){
nodeQueue.offer(curNode.right);
nodeValueQueue.offer(curNodeVaule - curNode.val);
}
}
}
return false;
}
}
4. 将有序数组转换为二叉搜索树
public class SortedArrayToBST {
/**
* 108. 将有序数组转换为二叉搜索树
*/
/*
* 二叉搜索树的中序遍历时一个递增序列。
* 高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
*/
public TreeNode sortedArrayToBST(int[] nums) {
/*
* 找到中间的那个元素作为根节点,然后这样一直递归下去,当左边的下标大于右边的下标时,返回null.
* 不能一边往左走,一边往右走,因为那样会导致子树高度差的绝对值大于1
*/
return helper(nums, 0, nums.length - 1);
}
private TreeNode helper(int[] nums, int left, int right) {
if (left > right) {
return null;
}
// 获取中间位置(偶数的话,就是中间靠左)
int mid = (right + left) / 2;
// 创建根节点
TreeNode root = new TreeNode(nums[mid]);
// 递归建立左边数组的根节点
root.left = helper(nums, left, mid - 1);
// 递归建立右边数组的根节点
root.right = helper(nums, mid + 1, right);
return root;
}
}
5. 二叉搜索树迭代器
public class BSTIterator {
/**
* 剑指 Offer II 055. 二叉搜索树迭代器
*/
private int idx = 0;
private List<Integer> valueList = new ArrayList<Integer>();
public BSTIterator(TreeNode root) {
if (root == null) {
return;
}
Deque<TreeNode> stack = new LinkedList<TreeNode>();
// stack.push(root);
TreeNode curNode = root;
while (!stack.isEmpty() || curNode != null) {
while (curNode != null) {
stack.push(curNode);
curNode = curNode.left;
}
curNode = stack.pop();
valueList.add(curNode.val);
curNode = curNode.right;
}
}
public int next() {
return valueList.get(idx++);
}
public boolean hasNext() {
return idx < valueList.size();
}
}
2. 位运算
1. 只出现一次的数字
public class SingleNumber {
/**
* 136. 只出现一次的数字
*/
// 线性时间复杂度:O(n)
/*
* 1)按位取反(~):对于任意 a,~a = -(a+1);
* 2)按位与(&):
* 3)按位异或(^) :
* 4)按位或(|)
*/
public int singleNumber1(int[] nums) {
/*
* 因为:任何数和 0 做异或运算,结果仍然是原来的数;任何数和其自身做异或运算,结果是 0;异或运算满足交换律和结合律
* 所以直接异或就好了。
*/
if (nums.length == 1) {
return nums[0];
}
int ans = nums[0];
for (int i = 1; i < nums.length; i++) {
ans = ans ^ nums[i];
}
return ans;
}
public int singleNumber2(int[] nums) {
/*
* 之前用哈希表做的。
*/
if (nums.length == 1) {
return nums[0];
}
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
map.put(nums[0], 1);
for (int i = 1; i < nums.length; i++) {
if (map.containsKey(nums[i])) {
map.remove(Integer.valueOf(nums[i]));
map.put(nums[i], 2);
} else {
map.put(nums[i], 1);
}
}
int one_num = 0;
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (entry.getValue() == 1) {
one_num = entry.getKey();
}
}
return one_num;
}
}
3. 双指针
1. 反转链表
public class ReverseList {
/**
* 剑指 Offer 24. 反转链表
*/
/**
* 栈
*/
public ListNode reverseList1(ListNode head) {
/*
* 用栈来实现,定义一个栈,链表从头结点开始入栈,最后出栈再重新连在一起
*/
// 边界处理
if (head == null) {
return null;
}
Deque<Integer> stack = new LinkedList<Integer>();
// 头节点复制一份
ListNode curNode = head;
while (curNode != null) {
stack.push(curNode.val);
curNode = curNode.next;
}
// 创建头节点
ListNode dummy = new ListNode(stack.pop());
// 头节点复制一份
curNode = dummy;
while (!stack.isEmpty()) {
ListNode next = new ListNode(stack.pop());
curNode.next = next;
curNode = curNode.next;
}
return dummy;
}
/**
* 一次迭代
*/
public ListNode reverseList2(ListNode head) {
/*
* 遍历列表,将每次遍历到的节点指向前一个结点
* 那么就需要首先为头节点定义一个
* 然后当前节点指向上一个节点的话,与后面的节点就断开了,所以需要一个节点存储后面的节点。
* 双指针:一个指针表示前一个节点,一个指针表示当前节点,当前节点的下一个节点指向前一个节点;
* 然后前一个节点先移到当前节点的位置,当前节点再往下移,最后当前节点为空,前一个节点是原始链表最后一个节点。
*/
// 定义两个指针
ListNode prev = null;
ListNode curNode = head;
while (curNode != null) {
// 用一个节点存储链表断开后面的节点
ListNode next = curNode.next;
// 当前节点的下一个节点指向前一个节点
curNode.next = prev;
// 更新指针
prev = curNode;
curNode = next;
}
return prev;
}
/**
* 递归
*/
public ListNode reverseList3(ListNode head) {
/*
* 核心是:nk.next.next = nk
* 相当于:nk+1.next = nk
* 反转了,头结点的下一个要指向空
*/
if (head == null || head.next == null) {
return head;
}
//
ListNode newHead = reverseList3(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
/**
* 递归
*/
public ListNode reverseList4(ListNode head) {
/*
* 每次传入两个节点(head, null)--> (head.next, head) --> …… (cur, pre):(当前节点,前一个节点)
* 当到最后是,进行回溯,将 head.next.next = head
*/
return recur(head, null);
}
private ListNode recur(ListNode cur, ListNode pre) {
if (cur == null) {
return pre;
}
ListNode res = recur(cur.next, cur);
cur.next = pre;
return res;
}
}
2. 删除链表的倒数第 n 个结点
public class RemoveNthFromEnd {
/**
* 剑指 Offer II 021. 删除链表的倒数第 n 个结点
*/
/**
* 双指针
*/
public ListNode removeNthFromEnd1(ListNode head, int n) {
/*
* 双指针——前后双指针
* 定义一个指针 pLeft 指向头节点,另一个指针 pRight 指向 k-1 个节点,当 pRight 为null时,pLeft正好指向待删除结点,然后删掉就好了
* 当添加哨兵节点后,就不需要在定义节点来记录待删除结点之前的节点了,因为 pLeft.next 才是待删除结点
*/
// 当链表长度为1时,删除节点后,链表为null,为了方便删除,给链表的头结点前加一个哨兵节点
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode pLeft = dummy;
ListNode pRight = head;
for (int i = 0; i < n - 1; i++) {
pRight = pRight.next;
}
while (pRight.next != null) {
pLeft = pLeft.next;
pRight = pRight.next;
}
pLeft.next = pLeft.next.next;
return dummy.next;
}
/**
* 栈
*/
public ListNode removeNthFromEnd2(ListNode head, int n) {
/*
* 链表入栈,当出栈到第k个节点时,删除
* 为了避免链表长度为1删除一个节点后链表为空的现象,还得加一个哨兵节点
*/
ListNode dummy = new ListNode(0);
dummy.next = head;
Deque<ListNode> stack = new LinkedList<ListNode>();
// 头节点复制一份
ListNode curNode = dummy;
// 节点入栈
while (curNode != null) {
stack.push(curNode);
curNode = curNode.next;
}
// 定义当前待删除节点
ListNode deletedNode = stack.peek();
while (n > 0) {
deletedNode = stack.pop();
n--;
}
stack.peek().next = deletedNode.next;
return dummy.next;
}
/**
* 用链表长度
*/
public ListNode removeNthFromEnd3(ListNode head, int n) {
/*
* 先加个头结点,然后获取到链表长度n,然后删除第n-k个节点
*/
// 加头节点
ListNode dummy = new ListNode(0, head);
int size = 0;
ListNode curNode = dummy.next;
while (curNode != null) {
size++;
curNode = curNode.next;
}
curNode = dummy;
for (int i = 0; i < size - n; i++) {
curNode = curNode.next;
}
curNode.next = curNode.next.next;
return dummy.next;
}
}
3. 删除排序链表中的重复元素
public class DeleteDuplicates {
/**
* 83. 删除排序链表中的重复元素
*/
/**
* 双指针
*/
public ListNode deleteDuplicates1(ListNode head) {
/*
* 双指针,定义一个指针pLeft指向头节点,pRight = pLeft.next
* 当pRight不为空时,一直循环
* 若 pLeft.val == pRight.val;那么pLift.next = pRight.next;
* 若 pLeft.val != pRight.val;那么pLeft、pRight都想下一个节点移动
*/
// 边界处理
if (head == null) {
return null;
}
ListNode pLeft = head;
ListNode pRight = pLeft.next;
while (pRight != null) {
if (pLeft.val != pRight.val) {
pLeft = pLeft.next;
pRight = pRight.next;
} else {
pLeft.next = pRight.next;
pRight = pRight.next;
}
}
return head;
}
/**
* 单指针
*/
public ListNode deleteDuplicates2(ListNode head) {
/*
* 只判断当前节点与他的下一个节点的关系;
* 若不等,当前节点后移;
* 若相等,curNode.next = curNode.next.next
* 这样循环时就不能只判断 当前节点不为null了,还要判断当前节点的下一个节点不为空;
*/
// 边界处理
if (head == null) {
return null;
}
ListNode curNode = head;
while (curNode != null && curNode.next != null) {
if (curNode.val != curNode.next.val) {
curNode = curNode.next;
} else {
curNode.next = curNode.next.next;
}
}
return head;
}
}
4. 环形链表
public class HasCycle {
/**
* 141. 环形链表
*/
/**
* 哈希表
*/
public boolean hasCycle1(ListNode head) {
/*
* 用哈希表,遍历链表,将每次遍历的节点存在哈希表中,若当前节点在哈希表中,那么返回true,否则,false
*/
// 边界判断
if (head == null) {
return false;
}
Map<ListNode, Integer> hashmap = new HashMap<ListNode, Integer>();
ListNode curNode = head;
while (curNode != null) {
if (!hashmap.containsKey(curNode)) {
hashmap.put(curNode, curNode.val);
curNode = curNode.next;
} else {
return true;
}
}
return false;
}
/**
* 快慢指针
*/
public boolean hasCycle2(ListNode head) {
/*
* 快慢指针,假设链表有个头结点,其中快指针一次走两个节点,慢指针一次走一个节点,当两个指针相遇时,证明有环
* 当快指针为null是,证明无环,两个指针初始化为头结点的下一个节点与第二个节点,这样就可以直接while循环,避免了初始化为同一个节点时的相遇
*/
// 边界处理
if (head == null || head.next == null) {
return false;
}
ListNode fastPointer = head.next; // 相当于 dummy.next.next
ListNode slowPointer = head; // 相当于 dummy.next
while (fastPointer != null && fastPointer.next != null) {
if (fastPointer == slowPointer) {
return true;
} else {
slowPointer = slowPointer.next;
fastPointer = fastPointer.next.next;
}
}
return false;
}
}
5. 排序链表
public class SortList {
/**
* 148. 排序链表
*/
public ListNode sortList1(ListNode head) {
/*
* 冒泡排序
* 定义两个指针
*/
if (head == null || head.next == null) {
return head;
}
ListNode curNode = head;
while (curNode != null) {
head = sort(head);
curNode = curNode.next;
}
return head;
}
private ListNode sort(ListNode head) {
ListNode curNode = head;
while (curNode.next != null) {
int a = curNode.val;
int b = curNode.next.val;
if (a > b) {
int temp = a;
curNode.val = b;
curNode.next.val = temp;
}
curNode = curNode.next;
}
return head;
}
/**
* 归并排序-自上往下分割合并排序
*/
public ListNode sortList2(ListNode head) {
/*
* 归并排序是分治的一种,将链表从中间分为两个字链表,将两个子链表排好序在合并,就相当于排好序了,子链表递归的分为子子链表;
* 当链表的长度小于1或等于1时,就不需要再进行拆分和排序了;
* 通过快慢指针,找到链表的中间节点
*/
// 边界处理
if (head == null || head.next == null) {
return head;
}
// 快慢指针找到中间节点;快指针一次走两个,满指针一次走一个,当快指针走完时,慢指针刚好走了一半。奇数个节点找到中点,偶数个节点找到中心左边的节点。
ListNode slow = head;
ListNode fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 将链表从slow处分成两个链表
ListNode temp = slow.next;
slow.next = null;
// 左右两个链表进行递归排序
ListNode left = sortList2(head);
ListNode right = sortList2(temp);
// 将已经分割后的链表进行合并
ListNode dummy = new ListNode(0); // 定义一个哨兵节点
ListNode res = dummy;
while (left != null && right != null) {
if (left.val < right.val) {
res.next = left;
left = left.next;
} else {
res.next = right;
right = right.next;
}
res = res.next;
}
res.next = left != null ? left : right;
return dummy.next;
}
}
4. 搜索
1. 二分查找
public class Search {
/**
* 704. 二分查找
*/
public int search(int[] nums, int target) {
/*
* 定义开始位置 start 与结束位置 end;mid = (start + end)/2
* 如果 nums[mid] > target ,那么目标值在左侧,end = mid-1
* 否则在右侧,start = mid+1
*/
int start = 0;
int end = nums.length - 1;
while (start < end) {
int mid = (start + end) / 2;
if (nums[mid] > target) {
end = mid - 1;
} else if (nums[mid] < target) {
start = mid + 1;
} else {
return mid;
}
}
return -1;
}
}
2. 二叉树的中序遍历
public class InorderTraversal {
private List<Integer> list;
/**
* 94. 二叉树的中序遍历
*/
/**
* 递归
*/
public List<Integer> inorderTraversal1(TreeNode root) {
/*
* 中序遍历是:左 -> 根 -> 右
*/
list = new ArrayList<Integer>();
dfs(root);
return list;
}
private void dfs(TreeNode root) {
/*
* 先往左递归,在添加值,再往右递归
*/
if (root == null) {
return;
}
dfs(root.left);
list.add(root.val);
dfs(root.right);
}
/**
* 迭代
*/
public List<Integer> inorderTraversal2(TreeNode root) {
/*
* 用栈来完成
*/
list = new ArrayList<Integer>();
// 边界处理
if (root == null) {
return list;
}
Deque<TreeNode> stack = new LinkedList<TreeNode>();
TreeNode curNode = root;
while (!stack.isEmpty() || curNode != null) {
// 先把所有左子树入栈
while (curNode!=null){
stack.push(curNode);
curNode = curNode.left;
}
curNode = stack.pop();
list.add(curNode.val);
curNode = curNode.right;
}
return list;
}
}
3. 对称的二叉树
public class IsSymmetric {
/**
* 剑指 Offer 28. 对称的二叉树
*/
public boolean isSymmetric1(TreeNode root) {
/*
* 对称二叉树,只要当前根节点的左子树 == 右子树就好了
* 用一个双端队列来完成,进行层序遍历,当节点为空时,添入"#"然后每次判断队尾与队首是否相同,若相同则继续,否则返回false
*/
if (root == null) {
return true;
}
Deque<TreeNode> nodeDeque = new LinkedList<TreeNode>();
Deque<String> valueDeque = new LinkedList<String>();
nodeDeque.offer(root);
// 为了方便判断,根节点的值入队两次
valueDeque.offer(String.valueOf(root.val));
valueDeque.offer(String.valueOf(root.val));
while (!nodeDeque.isEmpty()) {
int size = valueDeque.size() / 2;
for (int i = 0; i < size; i++) {
// 移除队尾与队首元素并判断是否相同
if (!valueDeque.pollFirst().equals(valueDeque.pollLast())) {
return false;
}
}
size = nodeDeque.size();
for (int i = 0; i < size; i++) {
TreeNode curNode = nodeDeque.poll();
// 添加节点的同时再加入节点的值,节点为空时值用"#"代替。
if (curNode.left != null) {
nodeDeque.offer(curNode.left);
valueDeque.offer(String.valueOf(curNode.left.val));
} else {
valueDeque.offer("#");
}
if (curNode.right != null) {
nodeDeque.offer(curNode.right);
valueDeque.offer(String.valueOf(curNode.right.val));
} else {
valueDeque.offer("#");
}
}
}
return true;
}
public boolean isSymmetric2(TreeNode root) {
/*
* 从根节点开始
* 层序遍历,每次入队时,控制好入队顺序,就不需要再创建节点值队列了
* 入队时先入左子树的左节点,再入右子树的右节点;然后再入左子树的右节点,最后入右子树的左节点,就可以一一对应了
*/
if (root == null) {
return true;
}
// 创建队列
Deque<TreeNode> deque = new LinkedList<TreeNode>();
// 根节点入栈两次
deque.offer(root);
deque.offer(root);
while (!deque.isEmpty()) {
// 获取左右子树
// 左右子树的可能性:1.都为null,那么将继续;2.其中一个为null,返回false;3.两个都不为null,判断值是否相等,不相等返回false;
TreeNode left = deque.poll();
TreeNode right = deque.poll();
// 当节点为叶子节点时,他的左右子树将为null,那么offer函数会将null添加到队列中去,所以要先判空
if (left == null && right == null) {
continue;
}
// 返回false的条件:其中一个节点为null一个不为null,或者两个就节点的值不相等
if ((left == null || right == null) || left.val != right.val) {
return false;
}
deque.offer(left.left);
deque.offer(right.right);
deque.offer(left.right);
deque.offer(right.left);
}
return true;
}
}
4. 二叉搜索树中第K小的元素
public class KthSmallest {
/**
* 230. 二叉搜索树中第K小的元素
*/
public int kthSmallest(TreeNode root, int k) {
/*
* 要找到最小元素,那么就先找到最左边的元素,然后左右左右往上走,走k个节点就好了
* 那很显然么,二叉搜索树是:左 < 根 < 右;
* 而中序遍历恰好是:左 -> 根 -> 右
* 所以直接中序遍历,找到第k个节点返回即可
*/
Deque<TreeNode> stack = new LinkedList<TreeNode>();
TreeNode curNode = root;
while (!stack.isEmpty() || curNode != null) {
while (curNode != null) {
stack.push(curNode);
curNode = curNode.left;
}
curNode = stack.pop();
k--;
if (k == 0) {
break;
}
curNode = curNode.right;
}
return curNode.val;
}
}
5. 二叉搜索树的第k大节点
public class KthLargest {
/**
* 剑指 Offer 54. 二叉搜索树的第k大节点
*/
public int kthLargest(TreeNode root, int k) {
/*
* 与第k小元素一样;利用后序遍历,即可找到第k大的元素
*/
Deque<TreeNode> stack = new LinkedList<TreeNode>();
TreeNode curNode = root;
while (!stack.isEmpty() || curNode!=null){
while (curNode!=null){
stack.push(curNode);
curNode = curNode.right;
}
curNode = stack.pop();
k--;
if(k == 0){
break;
}
curNode = curNode.left;
}
return curNode.val;
}
}
6. 搜索旋转排序数组
public class SearchSortArray {
/**
* 33. 搜索旋转排序数组
*/
public int search1(int[] nums, int target) {
/*
* 思路比较冗余;将 nums 划分为两个区间:大区间、小区间,用a,b表示
* 先判断 mid 在哪个区间中,用 nums[mid] 与 num[0] 判断,大于在a中,小于在b中
* 若在a中:当 nums[mid] < target 时,那么 target 必在 mid 的右边,所以直接 start = mid + 1;
* 当 nums[mid] > target 时,那么 target 有可能在 mid 左边,也有可能在区间b中,
* 所以此处要用 target 和 nums[0]进行判断,大于在 mid 左边,小于在区间b中;
* 同理,若在b中:当 nums[mid] > target 时,那么 target 必在 mid 的左边,所以直接 end = mid - 1;
* 当 nums[mid] < target 时,那么 target 有可能在 mid 右边,也有可能在区间中,
* 所以此处要用 target 和 nums[0]进行判断,小于在 mid 左边,大于在区间b中;
* 为了避免多次写nums[0] == target,在循环开始前先对 nums[0] 与 target进行判断,若相等,直接返回0;
*/
int start = 0;
int end = nums.length - 1;
if (target == nums[0]) {
return 0;
}
while (start <= end) {
int mid = (start + end) / 2;
if (nums[mid] >= nums[0]) {
if (nums[mid] > target) {
if (target > nums[0]) {
end = mid - 1;
} else {
start = mid + 1;
}
} else if (nums[mid] < target) {
start = mid + 1;
} else {
return mid;
}
} else if (nums[mid] < nums[0]) {
if (nums[mid] > target) {
end = mid - 1;
} else if (nums[mid] < target) {
if (target > nums[0]) {
end = mid - 1;
} else {
start = mid + 1;
}
} else {
return mid;
}
}
}
return -1;
}
public int search2(int[] nums, int target) {
/*
* 思路不变,对上面的代码进行优化;在a区间中,start有两种种情况,将end的那种情况写出来,不满足end的就是start了
* 在b区间中同理;end有两种情况,将满足start改变的条件写出来,其他的就是end的条件了
*/
int start = 0;
int end = nums.length - 1;
if (target == nums[0]) {
return 0;
}
while (start <= end) {
int mid = (start + end) / 2;
// 判断在那个区间中
if (nums[mid] == target) {
return mid;
} else if (nums[mid] >= nums[0]) {
if (nums[mid] > target && target > nums[0]) {
end = mid - 1;
} else {
start = mid + 1;
}
} else {
if (target < nums[0] && nums[mid] < target) {
start = mid + 1;
} else {
end = mid - 1;
}
}
}
return -1;
}
}
3 进阶学习
1. 排序
1. 合并两个有序数组
public class Merge {
/**
* 88. 合并两个有序数组
*/
public void merge1(int[] nums1, int m, int[] nums2, int n) {
/*
* 那就从后面往前比谁大,谁的值放到新数组的后面,若一个比完了,结束,将没比玩的移过去
* 此过程需要创建一个数组
*/
int[] arr = new int[n + m];
while (n != 0 && m != 0) {
if (nums1[m - 1] > nums2[n - 1]) {
arr[n + m - 1] = nums1[m - 1];
m--;
} else {
arr[n + m - 1] = nums2[n - 1];
n--;
}
}
if (n != 0) {
for (int i = 0; i < n; i++) {
arr[i] = nums2[i];
}
} else {
for (int i = 0; i < m; i++) {
arr[i] = nums1[i];
}
}
for (int i = 0; i < arr.length; i++) {
nums1[i] = arr[i];
}
}
public void merge2(int[] nums1, int m, int[] nums2, int n) {
/*
* 两个相比,大的往后面放,然后小的前移;最后再判断一下m移完,n没移完的情况。
*/
int t = n + m - 1;
while (n > 0 && m > 0) {
if (nums1[m - 1] > nums2[n - 1]) {
nums1[t] = nums1[m - 1];
m--;
} else {
nums1[t] = nums2[n - 1];
n--;
}
t--;
}
if (n != 0) {
for (int i = 0; i < n; i++) {
nums1[i] = nums2[i];
}
}
}
}
2. 有序数组的平方
public class SortedSquares {
/**
* 977. 有序数组的平方
*/
public int[] sortedSquares1(int[] nums) {
/*
* 暴力破解
*/
for (int i = 0; i < nums.length; i++) {
nums[i] = nums[i] * nums[i];
}
Arrays.sort(nums);
return nums;
}
public int[] sortedSquares2(int[] nums) {
/*
* 双指针,但要创建额外数组
*/
int i = nums.length - 1;
int[] arr = new int[nums.length];
int right = nums.length - 1;
int left = 0;
while (left < right) {
if (nums[left] * nums[left] >= nums[right] * nums[right]) {
arr[i] = nums[left] * nums[left];
left++;
} else {
arr[i] = nums[right] * nums[right];
right--;
}s
i--;
}
if (left == right) {
arr[i] = nums[right] * nums[right];
}
return arr;
}
}
3. 数组的相对排序
public class RelativeSortArray {
/**
* 1122. 数组的相对排序
*/
public int[] relativeSortArray1(int[] arr1, int[] arr2) {
/*
* 利用哈希表,先将 arr1 中的元素存储到哈希表中,存储规则:<元素值,元素出现的次数>
* 然后对 arr2 进行遍历,将与 arr2 中的元素相同的元素先进行存储,最后剩下一些没有在arr2中出现的元素
* 将他们存放到list中,对list进行排序,然后将list中的值在按照升序接到后面。
*/
Map<Integer, Integer> hashmap = new HashMap<Integer, Integer>();
// arr1存入哈希表
for (int i = 0; i < arr1.length; i++) {
if(!hashmap.containsKey(arr1[i])){
hashmap.put(arr1[i], 1);
}else {
hashmap.put(arr1[i], hashmap.get(arr1[i]) + 1);
}
}
int t = 0;
// arr2中的元素存取
for (int i = 0; i < arr2.length; i++) {
for (int j = 0; j < hashmap.get(arr2[i]); j++) {
arr1[t] = arr2[i];
t++;
}
hashmap.remove(arr2[i]);
}
// 创建一个list存放剩下的值
List<Integer> arrayList = new ArrayList<Integer>();
// 定义一个迭代器
Iterator<Integer> iterator = hashmap.keySet().iterator();
while (iterator.hasNext()){
Integer key = iterator.next();
for (int i = 0; i<hashmap.get(key);i++){
arrayList.add(key);
}
}
Collections.sort(arrayList);
for (int i = 0; i < arrayList.size(); i++) {
arr1[t] = arrayList.get(i);
t++;
}
return arr1;
}
/**
* 查表法
*/
public int[] relativeSortArray2(int[] arr1, int[] arr2) {
/*
* 因为arr1的值域为[0,1000],所以我们用一个长度为1001的数组来存放arr1中元素出现的次数
*/
int[] table = new int[1001];
for (int i = 0; i < arr1.length; i++) {
table[arr1[i]]++;
}
int t = 0;
// 将arr2中对应的元素先存放到数组中
for (int i = 0; i < arr2.length; i++) {
for (int j = 0; j < table[arr2[i]]; j++) {
arr1[t] = arr2[i];
t++;
}
table[arr2[i]] = 0;
}
// 把剩下的数组元素按照升序存放
for (int i = 0; i < table.length; i++) {
if(table[i] == 0){
continue;
}else {
for (int j = 0; j < table[i]; j++) {
arr1[t] = i;
t++;
}
}
}
return arr1;
}
}
4. 对链表进行插入排序
public class InsertionSortList {
/**
* 147. 对链表进行插入排序
*/
public ListNode insertionSortList(ListNode head) {
/*
* 创建一个额外链表dummy来存储结果;每次从head中取出头节点作为curNode
* 与dummy中的节点进行遍历比较,遇到大于curNode.val的节点,就将curNode插在他的前面,否则插在dummy末尾,并跳出;
* 直到head为空遍历结束。
*/
// 创建一个头结点,最为返回结果的开始
ListNode dummy = new ListNode(-5001); // 因为head中的值最小为-5000;
while (head != null) {
ListNode curNode = head;
head = head.next;
// 头节点复制一份
ListNode dummyTemp = dummy;
while (dummyTemp != null) {
if (dummyTemp.next == null || dummyTemp.next.val > curNode.val) {
curNode.next = dummyTemp.next;
dummyTemp.next = curNode;
break;
} else {
dummyTemp = dummyTemp.next;
}
}
}
return dummy.next;
}
}
5. 排序数组
public class SortArray {
/**
* 912. 排序数组
*/
/**
* 冒泡排序,不需要创建额外空间。会超时。
*/
public int[] bubbleSort(int[] nums) {
/*
* 题目要求是升序排序,所以每次将最大的放在后面;
* 优化:加一个标志位,记录当前轮数组中元素是否有变化,若没有,则直接返回
*/
boolean symbol = true;
// 第一个for循环控制的是比对的轮数,第二个for控制的是比对的位置。
for (int i = 0; i < nums.length && symbol; i++) {
symbol = false; // 若下面的for未执行,那么symbol为false,下轮就将退出循环。
for (int j = 0; j < nums.length - i; j++) {
// 每次将最大的放后面
if (j + 1 < nums.length && nums[j] > nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
symbol = false;
}
/*
// 每次将最小的放后面
if (j + 1 < nums.length && nums[j] < nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
symbol = false;
}
*/
}
}
return nums;
}
/**
* 快速排序,不需要额外空间
*/
public int[] quickSort(int[] nums) {
/*
* 快排思路:每次找一个基点,然后两个哨兵,一个从左往右走,另一个从右往左走;
* 如果右边那个哨兵找到比基点大的数则停下来,左边那个哨兵找到比基点小的数停下来,然后交换两个哨兵找到的数;
* 如果找不到最后两个哨兵就会碰到一起,最后交换基点和哨兵相遇的的地方的元素;
* 然后就将数组按照基点分成了左边比基点小,右边比基点大,然后递归左边和右边,最后的结果就是有序的了。
* 注意:此处一定要先移动右边的哨兵,不然当最后排好序时,最后的交换可能会出错
*/
quickSort(nums, 0, nums.length - 1);
return nums;
}
private void quickSort(int[] nums, int left, int right) {
if (left > right) {
return;
}
// 将基准设为中间的元素
int base = nums[left];
int l = left, r = right;
// 两个哨兵(i左边,j右边)没有相遇
while (l < r) {
// 一个哨兵往左走,找比基点大的数
while (nums[r] >= base && l < r) {
r--;
}
// 一个哨兵往右走,找比基点小的数
while (nums[l] <= base && l < r) {
l++;
}
// 如果满足 l<r 则交换
if (l < r) {
int temp = nums[l];
nums[l] = nums[r];
nums[r] = temp;
}
}
// 最后交换基点和 l、r 相遇的地方的元素
nums[left] = nums[l];
nums[l] = base;
quickSort(nums, left, l - 1); // 递归调用基准的左半边数组
quickSort(nums, l + 1, right); // 递归调用基准的右半边数组
}
/**
* 选择排序——直接选择排序,不需要额外空间。会超时。
*/
public int[] selectSort(int[] nums) {
/*
* 每次从数组中选出一个最大或最小的元素放在数组后面或者前面
*/
// 此处将最小的依次放在最前面
for (int i = 0; i < nums.length; i++) {
// 用下标记录最小值,方便后面的交换
int min = i;
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] < nums[min]) {
min = j;
}
}
// 交换
if (min != i) {
int temp = nums[i];
nums[i] = nums[min];
nums[min] = temp;
}
}
return nums;
}
/**
* 堆排序
* 大顶堆:双亲结点的值比每一个孩子结点的值都要大。根结点值最大
* 小顶堆:双亲结点的值比每一个孩子结点的值都要小。根结点值最小
*/
public int[] heapSort(int[] nums) {
/*
* 大顶堆:nums[i] >= nums[2i+1] && nums[i] >= nums[2i+2]
* 小顶堆:nums[i] <= nums[2i+1] && nums[i] <= nums[2i+2]
*/
// 构建一个大堆顶,此处 i 从 nums.length / 2 - 1 开始,当数组长度大于2时,for才会执行,才需要构建堆
for (int i = nums.length / 2 - 1; i >= 0; i--) {
sift(nums, i, nums.length);
}
// 将大顶堆的堆顶值放到数组后面,然后再继续构建大顶堆
for (int i = nums.length - 1; i > 0; i--) {
int temp = nums[i];
nums[i] = nums[0];
nums[0] = temp;
// 将最大值放到最后面之后,剩下的元素,再重新构建大顶堆
sift(nums, 0, i);
}
return nums;
}
private void sift(int[] nums, int parent, int len) {
// 取出当前元素,即根节点
int value = nums[parent];
// 从根节点的左子树开始,也就是 parent * 2+1 处开始,child 的 child 从 child*2+1 开始
for (int child = 2 * parent + 1; child < len; child = child * 2 + 1) {
// 左节点的值小于右节点的值时,将child指向右节点
if (child + 1 < len && nums[child] < nums[child + 1]) {
child++;
}
// 当子节点的值大于父节点的值,将子节点值赋给父节点(不用进行交换);
if (nums[child] > value) {
nums[parent] = nums[child]; // 父节点的值==子节点的值
parent = child; // 父节点在指向子节点
} else {
break;
}
nums[child] = value; // 上面没有进行交换,也就是nums[parent]的值在这块进行了交换。
}
}
/**
* 归并排序——分治法,需要额外空间
*/
public int[] mergeSort(int[] nums) {
/*
* 采用分治的思想,像数组不断分割,直至到单个元素,然后在比较合并
*/
// 创建一个 nums.length 长的数组
int[] temp = new int[nums.length];
mergeSort(nums, 0, nums.length - 1, temp);
return temp;
}
private void mergeSort(int[] nums, int left, int right, int[] temp) {
/*
* 递归分割
*/
//这里是递归结束的条件,我们是对半分,那当left==right的时候肯定大家都是只有一个元素了。
if (left < right) {
//对半分,比如总长度是10,left=0,right=9,mid=4确实是中间分了,0~4,5~9
//当长度9,left=0,right=8,mid=4,0~4,5~8
int mid = left + (right - left) / 2; // 这样写可以防止越界;
mergeSort(nums, left, mid, temp); // 左边归并排序
mergeSort(nums, mid + 1, right, temp); // 右边归并排序
// 将两个有序数组合并的操作
merge(nums, left, mid, right, temp);
}
}
private void merge(int[] nums, int left, int mid, int right, int[] temp) {
/*
*
*/
int i = left;
int j = mid + 1;
int t = 0; // 临时数组指针
while (i <= mid && j <= right) {
// 比较两个元素谁小,谁小谁先拷贝到temp;
if (nums[i] < nums[j]) {
temp[t++] = nums[i++];
} else {
temp[t++] = nums[j++];
}
}
// 将左边未拷完的拷到数组中
while (i <= mid) {
temp[t++] = nums[i++];
}
// 将右边未拷完的拷到数组中
while (j <= right) {
temp[t++] = nums[j++];
}
t = 0;
// 将temp中的元素拷到nums中去
while (left <= right) {
nums[left++] = temp[t++];
}
}
/**
* 插入排序,不需要额外空间
*/
public int[] insertSort(int[] nums) {
/*
* 插入排序就相当于从第二个元素开始,从后往前比较,如果前面的大,就将前面的元素往后移,
* 直到找到一个合适的位置,就把这个元素放到这个位置,直到数组有序。
*/
// i 为当前需要排序的元素,从第二个开始,直到最后一个。
for (int i = 1; i < nums.length; i++) {
int temp = nums[i]; // temp代表当前要插入的元素。
int j = 0; // 当下面的for循环
// nums[j] 代表的是temp之前的元素,跟那些元素进行比较,若temp比前面的元素小,那么将前面的元素往后移。
for (j = i - 1; j >= 0 && temp < nums[j]; j--) {
// 第一轮,nums[j+1] = nums[i] = temp
nums[j + 1] = nums[j]; // 将前面大的元素往后移
}
// 如果上面for不满足时
nums[j + 1] = temp;
}
return nums;
}
/**
* 计数排序,需要额外空间,相当于查表法
*/
public int[] countSort(int[] nums) {
/*
* 找到数组的最大值与最小值,建立一个长为 max-min+1 的数组
*/
int d = 0, max = nums[0], min = nums[0];
for (int i = 0; i < nums.length; i++) {
if (min > nums[i]) {
min = nums[i];
}
if (max < nums[i]) {
max = nums[i];
}
}
// 建立一个用于计数的数组
d = min;
int[] count_arr = new int[max - min + 1];
for (int i = 0; i < nums.length; i++) {
// count_arr 中的元素代表 nums 中的元素出现的次数。
count_arr[nums[i] - min]++;
}
int k = 0;
for (int i = 0; i < nums.length; ) {
// 在这里控制相同元素的插入,而且当count_arr中的元素值为0时,k加i不加
if (count_arr[k] > 0) {
nums[i] = k + min;
i++;
count_arr[k]--;
} else {
k++;
}
}
return nums;
}
/**
* 希尔排序,不需要额外空间
*/
public int[] shellSort(int[] nums) {
/*
* 插入排序的进阶版吧,最后的行为还是插入排序,只是把每次插入排序的作用数组改变了;
* 设原数组长度为n,每次插入排序的数组为:{n/2,n/2/2,……,1},这个也被称为希尔序列
* 最后的1不就是对原数组进行插入排序么,但是希尔序列每次取长度的二分之一并不是最优的选择。
*/
// 第一个循环决定比较的间隔
for (int d = nums.length / 2; d > 0; d = d / 2) {
// 第二个循环根据间隔将数组分为若干个序列。
for (int i = d; i < nums.length; i++) {
int temp = nums[i];
int j = 0;
// 第三个循环对若干个序列进行插入排序
for (j = i - d; j >= 0 && temp < nums[j]; j = j - d) {
nums[j + d] = nums[j];
}
nums[j + d] = temp;
}
}
return nums;
}
/**
* 桶排序,需要额外空间
*/
public int[] bucketSort(int[] nums) {
/*
* 桶排序:就是找到数组中的最大值与最小值,然后根据数组长度与最值创建若干个桶区间;
* 遍历数组,将每个元素放到它所对应的那个桶中去,最后对每个桶排序,然后再将排了序的元素拿出来合并在一起。
*/
// 边界处理
if (nums == null || nums.length == 0) {
return nums;
}
// 查找数组的最大值与最小值
int max = nums[0];
int min = nums[0];
for (int i = 1; i < nums.length; i++) {
max = Math.max(nums[i], max);
min = Math.min(nums[i], min);
}
// 计算桶的数量,并创建桶
int bucketNum = (max - min) / nums.length + 1; // 加1,是为了避免前面为0时,确保有一个桶。
// 构建一个具有指定容器数量的列表(大小为bucketNum)
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<ArrayList<Integer>>(bucketNum);
// 对每个桶初始化
for (int i = 0; i < bucketNum; i++) {
bucketArr.add(new ArrayList<Integer>());
}
// 遍历数组,将每个元素,存放到对应桶中去
for (int i = 0; i < nums.length; i++) {
int num = (nums[i] - min) / nums.length; // 这块不能加1,因为下标是从0开始的
bucketArr.get(num).add(nums[i]);
}
// 对每个桶进行排序
for (int i = 0; i < bucketArr.size(); i++) {
Collections.sort(bucketArr.get(i));
}
// 将桶中元素又退到原数组中去
int index = 0;
for (int i = 0; i < bucketArr.size(); i++) {
for (int j = 0; j < bucketArr.get(i).size(); j++) {
nums[index++] = bucketArr.get(i).get(j);
}
}
return nums;
}
}