前言
本篇博客是参考 学习视频 的笔记
1.复杂度分析
算法时间复杂度分析;算法空间复杂度分析;大O记法
1.时间复杂度分析
用来计算算法时间损耗情况
1.1.事后分析估算方法
将算法执行若干次,并计量执行算法所需要的时间
1.设置循环(如for循环),执行若干次算法
2.利用long start/end = System.currentTimeMills()
timeA = end - start
计算耗费时间
显然,此方法只适用于小型算法
1.2.时前分析估算方法
在计算机编写程序前,通过统计方法对算法耗时进行估算
一门高级语言编写的程序在计算机上运行所损耗的时间取决于:
1.算法采用的策略与方案 2.编译产生的代码质量 3.问题的输入规模 4.机器执行指令的速度
2.空间复杂度分析
用来计算算法内存占用情况
2.1.基本数据类型内存占用
单位:字节(Byte)= 8比特(bit)
类型 | 内存 | 类型 | 内存 | 类型 | 内存 | 类型 | 内存 | 类型 | 内存 | 类型 | 内存 | 类型 | 内存 | 类型 | 内存 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
byte | 1 | short | 2 | int | 4 | long | 8 | float | 4 | double | 8 | boolean | 1 | char | 2 |
计算机访问内存的方式:一次一个字节
2.2.实例化对象的内存占用
Java中数组被先定为对象
Date date = new Date()
- .一个引用(机器地址)需要8个字节表示
对象变量
date
,需要8个字节表示
- 每个对象自身需要占用16个字节
除了对象内部存储的数据占用内存,对象的自身占用需要16个字节
new Date()
需要16个字节保存对象的头信息
- 当内存装不下数据时,会以8字节为单位,进行填充内存
如:现有17字节的数据需要装入16字节内存,装不下,系统将会自动增加8字节内存,也就是24个字节的内存来装着17个字节的数据
- Java中数组被限定为对象
一个原始数据类型的数组一般需要24字节的头信息(16字节自身对象开销,4字节保存长度,4字节填充空余的字节)
3.函数的渐进增长
对于函数f(n)、g(n),存在一个整数N,当n>N时,f(n)>g(n)
随着输入规模的增大:
1.算法的常数操作可以忽略不计
2.与最高次项相乘的常数可以忽略
3.算法中n的最高次幂越小,算法效率越高
4.大O记法
使用O()表示时间/空间复杂度的记法:O(f(n)) = T(n)
一般情况下,随着输入规模n的增大,T(n)增长最慢的算法为最优算法
执行次数=执行时间
对于Java这类在电脑这类拥有较大内存的计算机上运行的高级语言,讨论算法空间复杂度没有多大意义
4.1.推导大O阶的标识法的规则:
- 用常数1取代运行时间中的所有加法常数
- 在修改后的运行次数中,只保留高阶项
- 如果高阶项存在,且常数因子不为1,则 去除 与这个项相乘的常数
4.2.常见的大O阶
- 常数阶O(1)
int n = 999; //执行1次
int m = 0; //执行1次
- 线性阶O(n)
int n = 999; //执行1次
int m = 0; //执行1次
for (int i=0;i < n;i++){
m += i; //执行n次
}
- 平方阶O(n^2)
int n = 999; //执行1次
int m = 0; //执行1次
for (int i=0;i < n;i++){
m += i; //执行n次
for (int j=n;j > 0;j--){
m++; //执行n次
}
}
- 立方阶O(n^3)
- 对数阶O(logn)
int n = 999; //执行1次
int m = 0; //执行1次
for (int i=1;i <= n;i*=2){
m+=i; //执行log2(n)次
}
在大O分析时,我们会忽略底数,因为无论底数为多少,当随着n增大时,增长趋势一样
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3)
4.3.最坏情况分析
(没做特殊要求时)运行时间都是指在最坏情况下的运行时间
最坏情况
是一种保证,即使在最坏情况下,也能正常提供服务
如:在一个含有n个元素的列表中寻找目标元素
最好情况:第一个元素就是目标元素O(1)
平均情况:O(n/2)
最坏情况:查找的最后一个元素才为目标元素O(n)
2.递归简介
递归就是套娃
递归:
- 一个问题的解可以分解为几个子问题的解
- 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一致
- 存在终止条件
递归方法的组成部分:
- 终止条件: 什么条件下,方法不调用方法本身
- 递归条件: 什么条件下,方法会调用方法本身
递归的优缺点:
- 表达能力强,写起来很简洁
- 方法反复调用,大量入栈和出栈操作,空间复杂度较高,有堆栈溢出的问题
过多的方法调用,耗时也会增加,存在重复子问题计算问题
3.习题(递归 || 双指针)
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
使用递归求解
递归公式:
class Solution {
//存储子问题已经计算过的结果
private Map<Integer,Integer> map = new HashMap<>();
public int climbStairs(int n) {
/*
递归终止条件
*/
if(n == 1) return 1;
if(n == 2) return 2;
//如果map中已经有了结果,就直接返回结果
if(map.get(n) != null) return map.get(n);
//map中找不到结果,就进行计算
else{
int result = climbStairs(n-1) + climbStairs(n-2);
map.put(n,result);
return result;
}
}
}
为什么要用HashMap?
使用HashMap用来存储子问题已经计算过的结果,防止二次计算:
循环求解,自底向上累加
根据递归公式:
可以发现规律:
class Solution {
/**
循环求解,自底向上累加
*/
public int climbStairs(int n) {
if(n == 1) return 1;
if(n == 2) return 2;
int pre = 1;
int curr = 2;
int sum = 0;
for (int i = 3; i <= n; i++){
sum = pre + curr;
pre = curr;
curr = sum;
}
return sum;
}
}
斐波那契数列: 1、1、2、3、5、8、13、21、34、……
两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
思路1——暴力穷举: 把数组中每个数字都与其他数字进行相加,并与目标值进行对比。
class Solution {
/**
暴力穷举
*/
public int[] twoSum(int[] nums, int target) {
//存两个目标索引
int[] result = 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){
result[0] = i;
result[1] = j;
return result;
}
}
}
return result;
}
}
思路2——hash表避免第二次扫描
暴力穷举的内部循环将之前一次循环扫描过的数重新扫描了一遍,很消耗时间,所以我们可以用一个HashMap来存储之前扫描过的数,减少扫描时间。
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] result = new int[2];
int len = nums.length;
//map的key存数字,value存索引
Map<Integer,Integer> map = new HashMap<>(len - 1);
map.put(nums[0],0);
for (int i = 1; i < len; i++){
int another = target - nums[i];
if(map.containsKey(another)){
return new int []{i,map.get(another)};
}
map.put(nums[i],i);
}
//代码是不会运行到这块儿,随便返回就行
return result;
}
}
合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
思路1——使用Arrays.sort
class Solution {
/**
使用Arrays的sort方法
*/
public void merge(int[] nums1, int m, int[] nums2, int n) {
for (int i = 0; i < n; i++){
nums1[m + i] = nums2[i];
}
Arrays.sort(nums1);
}
}
思路2——双指针
思路一中的方法并没有利用到两个给定数组的有序性,浪费大量时间重排。
public void merge(int[] nums1, int m, int[] nums2, int n) {
int k = m+n;
//临时存储两数组
int[] tmp = new int[k];
//i1:数组1的指针,i2:数组2的指针
for (int i = 0, i1 = 0, i2 = 0; i < k; i++) {
if (i1 >= m){//数组1数已经被取完,直接存数组2的数
tmp[i] = nums2[i2++];
}else if (i2 >= n){//数组2数已经被取完,直接存数组1的数
tmp[i] = nums1[i1++];
}else if (nums1[i1] > nums2[i2]){//数组1>数组2
tmp[i] = nums2[i2++];
}else {
tmp[i] = nums1[i1++];
}
}
for (int i = 0; i < k; i++) {//拷贝数据
nums1[i] = tmp[i];
}
}
思路3——双指针(不用临时数组)
取消临时数组,双指针都倒序比较两个数组的数字,从而进行排序,直接将数组2插入数组1,不需要引入临时数组。
public void merge(int[] nums1, int m, int[] nums2, int n) {
int k = m+n;
for (int i = k-1, i1 = m-1, i2 = n-1; i >= 0; i--) {
if (i1 < 0){//nums1已经取完,完全取nums2的值
nums1[i] = nums2[i2--];
}else if (i2 < 0){
break;
}else if (nums1[i1] > nums2[i2]){
nums1[i] = nums1[i1--];
}else {
nums1[i] = nums2[i2--];
}
}
}
移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
思路——双指针
指针i用来遍历数组,第一次遍历指针j用来记录非0数字索引。
class Solution {
public void moveZeroes(int[] nums) {
if (nums == null) return;
int len = nums.length;
int j = 0;
//将非0数字都移到前面
for (int i=0; i < len; i++){
if (nums[i] != 0){
//将非0数字全部移动到 前面
nums[j++] = nums[i];
}
}
//填充0
for (int i = j; i < len; i++){
nums[i] = 0;
}
}
}
找到所有数组中消失的数字
给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。
进阶:你能在不使用额外空间且时间复杂度为 O(n) 的情况下解决这个问题吗? 你可以假定返回的数组不算在额外空间内。
思路1——做标记
说明:
- 下标先到0位,数字为4,将4-1=3 ==> 将下标为3的元素设为7+8 = 15
- 下标来到1位,数字为3,将3-1=2 ==> 将下标为2的元素设为2+8 = 10
- 按照上述步骤依次遍历玩整个数组
- 最终,为正数的索引即为没出现在数组中的数字
-1 是因为数组下标从0开始,而整数范围区间是从1开始
将数字+8(8是数组长度,也可以是其他任意好做标记的数字)是为了标记此数字在数组中出现过
注意: 可能出现数组中数字重复现象,故而需要判断索引元素之前是否被修改过。
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
int len = nums.length;
for (int num : nums){
int j = (num - 1)%len;//还原数值后再加,防止溢出
nums[j] += len;
}
List<Integer> res = new ArrayList<>();
for (int i = 0; i < len; i++){
if (nums[i] <= len){
res.add(i+1);
}
}
return res;
}
}
注意:当我们遍历到某个位置时,其中的数可能已经被增加过,因此需要对 nn 取模来还原出它本来的值。
合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路1——循环+双指针
/**
* 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 mergeTwoLists(ListNode list1, ListNode list2) {
//当其中一个链表为空时,直接返回另一个链表
if (list1 == null) return list2;
if (list2 == null) return list1;
ListNode res = new ListNode(0);
ListNode tmp = res;
//比较链表排序插入
while (list1 != null && list2 != null){
if (list1.val > list2.val){
tmp.next = list2;
list2 = list2.next;
}else {
tmp.next = list1;
list1 = list1.next;
}
tmp = tmp.next;
}
//当其中一个链表依旧插完,另一个链表还有节点的时候,直接将tmp的指针指向链表
if (list1 != null){
tmp.next = list1;
}
if (list2 != null){
tmp.next = list2;
}
return res.next;
}
}
思路2——循环+双指针
与双指针思想类似,不过用的递归实现
/**
* 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 mergeTwoLists(ListNode list1, ListNode list2) {
/**
递归
*/
//当其中一个链表为空时,直接返回另一个链表
if (list1 == null) return list2;
if (list2 == null) return list1;
if (list1.val < list2.val){
list1.next = mergeTwoLists(list1.next,list2);
return list1;
}
list2.next = mergeTwoLists(list1,list2.next);
return list2;
}
}
删除排序链表中的重复元素
给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
思路1
因为是已排序链表,所以,相同的元素只会聚在一起,所以只需要将链表指针指向下下个节点就可以实现删除重复元素。
/**
* 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 deleteDuplicates(ListNode head) {
if (head == null) return head;
ListNode curr = head;
while (curr.next != null){
if (curr.val == curr.next.val){
curr.next = curr.next.next;
}else {
curr = curr.next;
}
}
return head;
}
}
思路2——递归
/**
* 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 deleteDuplicates(ListNode head) {
//三元运算涉及到head.next,所以此处需要判断head.next是否为空
if (head == null || head.next == null) return head;
head.next = deleteDuplicates(head.next);
//当前节点与下一节点元素相同,就返回下一节点
return head.val == head.next.val ? head.next : head;
}
}
环形链表
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。
注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
思路——hash表查表
将已经遍历过的节点存入hash表,每次遍历的时候都与hash表中的节点进行比较,如果存在即为存在环。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null) return false;
Map<ListNode,Integer> map = new HashMap<>();
ListNode tmp = head;
while (tmp.next != null){
if (map.containsKey(tmp)) return true;
else map.put(tmp,1);
tmp = tmp.next;
}
return false;
}
}
进阶:你能用 O(1)(即,常量)内存解决此问题吗?
思路2——弗洛伊德解法(快慢指针)
慢指针一次移动一个节点,快指针一次移动两个节点,如果链表存在环,那么这两个指针最终一定会相遇。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null) return false;
ListNode fast = head, slow = head;
while (fast.next != null && fast.next.next != null){
slow = slow.next;
fast = fast.next.next;
if (fast == slow) return true;
}
return false;
}
}
环形链表 II
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
思路——快慢指针
首先,同 环形链表 相同,先确认是否有环存在(快慢指针相遇,但此节点不一定是开始节点)
然后,重新把慢指针指向链表的头节点,快慢指针继续移动(此时快慢指针移动速度都为1)
这时,快慢指针再次相遇的节点,就是开始节点。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null) return null;
ListNode fast = head, slow = head, meet = null;
boolean flag = false;
while (fast.next != null && fast.next.next != null){
slow = slow.next;
fast = fast.next.next;
//快慢指针相遇,慢节点重回首节点,标志位设为true
if (fast == slow) {
slow = head;
flag = true;
break;
}
}
//快慢指针速度设为1,相遇点为环起始位置
if (flag){
while (fast != slow){
fast = fast.next;
slow = slow.next;
}
meet = slow;
}
return meet;
}
}
相交链表
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
图示两个链表在节点 c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
思路1——暴力穷举
将链表A中的每一个节点地址都与链表B中的每一个节点地址进行比较,如果存在相等的就说明相交
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode tmpB = headB;
while (true){
if (headA == null) break;
if (tmpB != null){
if (headA == tmpB){
return headA;
}else {
tmpB = tmpB.next;
}
}else {
headA = headA.next;
tmpB = headB;
}
}
return null;
}
}
思路2——使用hash表查表
将一个链表的所有节点存入hash表,然后遍历另一个链表,判断hash表中是否有相同节点,有的话就返回此节点。
进阶:你能否设计一个时间复杂度 O(m + n) 、仅用 O(1) 内存的解决方案?
思路3——双指针
大致思路如下,两个指针分别遍历两个链表,当遍历一个链表之后,指针会开始遍历另外一个链表,直到最终两个指针相遇,那个相遇的节点就是相交点。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode pA = headA, pB = headB;
//这条链表遍历完就跳转到另外一个链表
while(pA != pB){
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
//两个指针相等时,返回任意一个指针
return pA;
}
}
思路4——消除多余长度(双指针)
因为两个链表可能存在长度差,那么就获取此差值,然后让长链表从差值位置处开始遍历,这样就能移动相同位置
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int l1 = 0, l2 = 0, diff = 0;
ListNode head1 = headA, head2 = headB;
//两个循环统计出链表长度
while (head1 != null){
l1++;
head1 = head1.next;
}
while (head2 != null){
l2++;
head2 = head2.next;
}
//获取长度差值,重新head1指向较长的链表
if (l1 < l2){
head1 = headB;
head2 = headA;
diff = l2 - l1;
}else {
head1 = headA;
head2 = headB;
diff = l1 - l2;
}
//让长度较长的链表首先移动指针位置(差值个位置)
for (int i = 0; i < diff; i++){
head1 = head1.next;
}
while (head1 != null && head2 != null){
if (head1 == head2) return head1;
head1 = head1.next;
head2 = head2.next;
}
return null;
}
}
反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
注意: 这是一个单链表,前一个节点可以指向后一个节点,但是后一个节点不能直接指向前一个节点。
思路——双指针
/**
* 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 preNode = null;
ListNode curr = head;
while (curr != null){
ListNode next = curr.next;
curr.next = preNode;
preNode = curr;
curr = next;
}
return preNode;
}
}
回文链表
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
注意: 这是一个单链表
进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
思路——快慢指针(反转链表)
回文链表镜像对称,讲链表后半部分进行反转,然后进行两部分对比。
/**
* 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 boolean isPalindrome(ListNode head) {
ListNode fast = head, slow = head;
while (fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
//当链表是奇数个节点,把正中节点归到左边
if (fast != null){
slow = slow.next;
}
slow = reverse(slow);
//将fast移到初始节点
fast = head;
while (slow != null){
if (fast.val != slow.val){
return false;
}
fast = fast.next;
slow = slow.next;
}
return true;
}
/**
反转链表
*/
private ListNode reverse(ListNode head){
ListNode pre = null;
while (head != null){
ListNode next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
}
链表的中间结点
给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
思路1——先扫描长度,后扫描中间节点
/**
* 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 middleNode(ListNode head) {
if (head == null) return null;
ListNode list1 = head, list2 = head;
int len = 0, mid = 0;
//扫描长度
while (list1 != null){
len++;
list1 = list1.next;
}
mid = len/2 + 1;
for (int i = 1; i < mid; i++){
list2 = list2.next;
}
return list2;
}
}
思路2——快慢指针
快指针到达末端(偶数链表会到达null,奇数链表会到达最后一个极点)后,两个指针停止移动,慢指针所在位置就是中间节点。
/**
* 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 middleNode(ListNode head) {
ListNode fast = head, slow = head;
while (fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
思路1——hash表查表
遍历链表,将链表节点存入hash表并获取链表长度。
思路2——倒数n位=链表长度-正数位数
第一次遍历,获取链表长度;第二次遍历,确定节点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
if (head == null) return null;
int len = 0;
ListNode list1 = head, list2 = head;
//遍历长度
while (list1 != null){
len++;
list1 = list1.next;
}
//定位节点
for (int i = 0; i < len-k; i++){
list2 = list2.next;
}
return list2;
}
}
思路3——快慢指针
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
if (k <= 0 || head == null) return null;
ListNode p1 = head, p2 = null;
//p1指针移动k-1位
for (int i = 1; i < k; i++){
if (p1 != null) p1 = p1.next;
}
//p1、p2指针一起向链表末移动
while (p1 != null){
if (p2 == null){
p2 = head;
}else {
p2 = p2.next;
}
p1 = p1.next;
}
//返回p2
if (p2 != null){
return p2;
}
return null;
}
}
用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x)
将元素 x 推到队列的末尾int pop()
从队列的开头移除并返回元素int peek()
返回队列开头的元素boolean empty()
如果队列为空,返回 true ;否则,返回 false
说明:
- 你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
- 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
class MyQueue {
//定义输入栈和输出栈
private static Stack<Integer> inStack;
private static Stack<Integer> outStack;
//构造器
public MyQueue() {
inStack = new Stack<Integer>();
outStack = new Stack<Integer>();
}
//队列末尾添加元素(入栈)
public void push(int x) {
inStack.push(x);
}
//队列开头移除元素(出栈)
public int pop() {
if (outStack.isEmpty()){
in2out();
}
return outStack.pop();
}
//返回队列开头元素
public int peek() {
if (outStack.isEmpty()){
in2out();
}
return outStack.peek();
}
//是否为空
public boolean empty() {
return inStack.isEmpty() && outStack.isEmpty();
}
//将输入栈弹出压入输出栈
private void in2out(){
while (!inStack.isEmpty()){
outStack.push(inStack.pop());
}
}
}
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = new MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* boolean param_4 = obj.empty();
*/
字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。
思路
class Solution {
int ptr;
public String decodeString(String s) {
//LinkedList作为栈
LinkedList<String> stk = new LinkedList<String>();
ptr = 0;
while (ptr < s.length()) {
char cur = s.charAt(ptr);
//判断是否为数字
if (Character.isDigit(cur)) {
/**
处理数字,使数字完整
*/
String digits = getDigits(s);
stk.addLast(digits);
} else if (Character.isLetter(cur) || cur == '[') {
/**
处理普通字符和 [
*/
stk.addLast(String.valueOf(s.charAt(ptr++)));
} else {
/**
遇见了 ] 处理相匹配的 [ 之间的字符
*/
++ptr;
//使用另一个 list,将字符串组合
LinkedList<String> sub = new LinkedList<String>();
while (!"[".equals(stk.peekLast())) {
sub.addLast(stk.removeLast());
}
/**
因为栈的特点,导致组合的字符串
和原本的字符串相比是倒序的
所以需要翻转一次
*/
Collections.reverse(sub);
// 左括号出栈
stk.removeLast();
// 此时栈顶为当前 sub 对应的字符串应该出现的次数
int repTime = Integer.parseInt(stk.removeLast());
StringBuffer t = new StringBuffer();
String o = getString(sub);
// 构造字符串
while (repTime-- > 0) {
t.append(o);
}
// 将构造好的字符串入栈
stk.addLast(t.toString());
}
}
return getString(stk);
}
//获取字符串中的所有数字
public String getDigits(String s) {
StringBuffer ret = new StringBuffer();
while (Character.isDigit(s.charAt(ptr))) {
ret.append(s.charAt(ptr++));
}
return ret.toString();
}
public String getString(LinkedList<String> v) {
StringBuffer ret = new StringBuffer();
for (String s : v) {
ret.append(s);
}
return ret.toString();
}
}