算法1.1

前言

本篇博客是参考 学习视频 的笔记

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)

类型内存类型内存类型内存类型内存类型内存类型内存类型内存类型内存
byte1short2int4long8float4double8boolean1char2

计算机访问内存的方式:一次一个字节

2.2.实例化对象的内存占用

Java中数组被先定为对象

Date date = new Date()
  1. .一个引用(机器地址)需要8个字节表示

对象变量date,需要8个字节表示

  1. 每个对象自身需要占用16个字节

除了对象内部存储的数据占用内存,对象的自身占用需要16个字节
new Date()需要16个字节保存对象的头信息

  1. 当内存装不下数据时,会以8字节为单位,进行填充内存

如:现有17字节的数据需要装入16字节内存,装不下,系统将会自动增加8字节内存,也就是24个字节的内存来装着17个字节的数据

  1. 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取代运行时间中的所有加法常数
  2. 在修改后的运行次数中,只保留高阶项
  3. 如果高阶项存在,且常数因子不为1,则 去除 与这个项相乘的常数

4.2.常见的大O阶

  1. 常数阶O(1)
int n = 999;               //执行1次
int m = 0;                 //执行1次
  1. 线性阶O(n)
int n = 999;               //执行1次
int m = 0;                 //执行1次
for (int i=0;i < n;i++){   
    m += i;                //执行n次
}
  1. 平方阶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次
    }               
}
  1. 立方阶O(n^3)
  2. 对数阶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();
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

364.99°

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值