数据结构与算法(数组、链表、栈、队列、树)

数据结构与算法

时间复杂度

  • 只关注循环次数最多的一段代码
  • 总循环度等于最高阶项的复杂度
  • 嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

O(1)n)n)

递归

使用递归的条件

  • 一个问题的解可以分解为几个子问题的解
  • 这个问题与分解后的子问题,除了数据规模不同,求解思路完全一样
  • 存在终止条件

例题

数组加递归

1.1 爬楼梯

leetcode Hot100 第70题

题目

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

个人解析

根据题目可以知道,每次只能爬1或者2个台阶
所以可以得到  f(1) = 1;   f(2) = 2;
那么首先可以确定的是,因为每次只有1或者2两个选择,那么从两个台阶开始,每增加一阶台阶
设当前为台阶数为n,那么爬1阶台阶的可能的次数就是(n-1)*1
爬两阶因为可以选择爬一阶或者两阶,所以从n-2爬会有两种可能,那就是(n-2)*2
但是又因为n-2阶爬一阶时,就会与n-1重复,所以n-2去重后只剩爬两阶的可能
所以n-2阶开始爬,可以算作n-1爬一阶的方法数+n-2
所以爬n阶的方法数就等于  爬到n-1的方法数+爬到n-2的方法数
所以得到分段公式
f(1) = 1;						n = 1
f(2) = 2;						n = 2
f(n) = f(n-1) + f(n-2);			n > 2

代码解析

递归+动态规划

单方面使用递归,会使时间复杂度变高

public class ClimbStairs {

    public static void main(String[] args) {
        /**
         * 创建map存储重复数据,防止多次运算拖慢速度
         * 因为每次求n阶时,都会对n阶之前的结果进行运算,导致效率很低,所以使用动态规划创建map记录数据
         */
        Map<Integer, Integer> map = new HashMap<>();
        System.out.println(climb(45, map));
    }

    private static int climb(int n, Map<Integer, Integer> map){
        if (n == 1){
            map.put(n, 1);
            return 1;
        }
        if (n == 2){
            map.put(n, 2);
            return 2;
        }
        int value = 0;
        if (map.isEmpty()){
            value = climb(n - 1, map) + climb(n - 2, map);
            map.put(n, value);
        }else {
            value = map.get(n-1) + map.get(n - 2);
        }

        return value;
    }

}
1.2 斐波那契数列

剑指offer 第10 I 题

题目

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:
 *		f(n) = 0;                   n = 0;
 *      f(n) = 1;                   n = 1;
 *      f(n) = f(n - 1) + f(n - 2); n >= 2;
斐波那契数列由 01 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1

代码

/**
 * @author zwj
 * @description 斐波那契数列
 *      f(n) = 0;                   n = 0;
 *      f(n) = 1;                   n = 1;
 *      f(n) = f(n - 1) + f(n - 2); n >= 2;
 * @location 剑指offer 第10 I题
 * @date 2022/7/22 - 14:40
 */
public class ClimbStairsPro {

    public static void main(String[] args) {
        System.out.println(numberCol(45));

    }

    public static int numberCol(int n){
        /**
         * for循环解决问题
         * 1.建立两个变量存放算出的数据,避免重复计算
         * 2.每次计算完成后将值赋给变量
         */

        int preVal = 0;
        int postVal = 1;
        //结果值
        int value = 0;
        //判断0和1
        if (n == 0) return preVal;
        if (n == 1) return postVal;
        for (int i = 2; i <= n; i++) {
            value = (preVal + postVal) % 1000000007;
            //将n-1的值赋给pre,将新值赋给post
            preVal = postVal;
            postVal = value;
        }
        return value;

    }

}
2 两数之和

leetcode Hot100 第1题

题目

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

个人解析

解析一:
	暴力破解,数组循环
解析二:
	使用map,因为map是数组加链表加红黑树的结构,插入数据速度快,获取数据相对于纯链表也快
	每次拿数据和map中的数据进行相加比对,没有就放入map集合

代码

/**
 * @author zwj
 * @description 两数之和
 * @location leetcode HOT100 第一题
 * @date 2022/7/22 - 15:09
 */
public class TwoNumSum {

    public static void main(String[] args) {
        int[] num = {5, 8, 6, 9, 11, 55, 7, 21};
        Stream.of(byArray(num, 76)).forEach(System.out::println);
        Stream.of(byMap(num, 76)).forEach(System.out::println);

    }

    //数组暴力破解
    public static Integer[] byArray(int[] num, int value){
        for (int i = 0; i < num.length; i++) {
            for (int j = 0; j < num.length; j++) {
                if (num[i] + num[j] == value){
                    return new Integer[]{i, j};
                }
            }
        }
        return new Integer[0];
    }

    //Map
    public static Integer[] byMap(int[] num, int value){
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < num.length; i++) {
            if (map.containsKey(value - num[i])){
                return new Integer[]{map.get(value - num[i]), i};
            }
            map.put(num[i], i);
        }
        return new Integer[0];
    }
}
3 合并两个有序数组

leetcode 第88题

题目

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。

请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。

个人解析

解析一:
	数组循环放入,然后使用sort方法进行排序
解析二:
	双指针进行判断,创建新的临时数组,将小的数插入到新数组,指针后移,没插入的数组指针不移。
	如果指针在某个数组移动到最后,则将另一个数组剩余数据顺序插入
	时间复杂度O(m+n)
解析三:
	因为数组一的长度是两个数组的长度和,所以,可以利用数组一的空间,节省临时数据占用的内存
	双指针从数组尾部开始比较,从数组一的尾部开始插入

代码

/**
 * @author zwj
 * @description 合并有序数组
 * @location leetcode 第88题
 * @date 2022/7/22 - 16:20
 */
public class MergeArray {

    public static void main(String[] args) {
//        int[] byArray = byArray(new int[]{1, 2, 3, 0, 0, 0}, 3, new int[]{2, 3, 5}, 3);
//        for (int i : byArray) {
//            System.out.println(i);
//        }

//        int[] byASCNode = byASCNode(new int[]{1, 2, 3, 0, 0, 0}, 3, new int[]{2, 3, 5}, 3);
//        for (int i : byASCNode) {
//            System.out.println(i);
//        }

        int[] byDESCNode = byDESCNode(new int[]{1, 2, 3, 0, 0, 0}, 3, new int[]{2, 3, 5}, 3);
        for (int i : byDESCNode) {
            System.out.println(i);
        }
    }

    /**
     * 数组暴力破解
     * @param num1  数组一
     * @param m     数组一有效值
     * @param num2  数组二
     * @param n     数组二有效值
     * @return
     */
    public static int[] byArray(int[] num1, int m, int[] num2, int n){
        for (int i = 0; i < num2.length; i++) {
            num1[m + i] = num2[i];
        }
        Arrays.sort(num1);
        return num1;
    }

    /**
     * 双指针正向比较
     * @param num1  数组一
     * @param m     数组一有效值
     * @param num2  数组二
     * @param n     数组二有效值
     * @return
     */
    public static int[] byASCNode(int[] num1, int m, int[] num2, int n){
        int[] temp = new int[m + n];
        //创建指针
        int first = 0;
        int second = 0;

        for (int i = 0; i < m + n; i++) {
            if (first >= m){
                temp[i] = num2[second];
                second++;
            }else if (second >= n){
                temp[i] = num1[first];
                first++;
            }else if (num1[first] >= num2[second]){
                temp[i] = num2[second];
                second++;
            }else {
                temp[i] = num1[first];
                first++;
            }
        }


        for (int i = 0; i < m + n; i++) {
            num1[i] = temp[i];
        }
        return num1;
    }

    /**
     * 双指针反向比较   ---不创建临时数组
     * @param num1  数组一
     * @param m     数组一有效值
     * @param num2  数组二
     * @param n     数组二有效值
     * @return
     */
    public static int[] byDESCNode(int[] num1, int m, int[] num2, int n){
        //创建指针
        int first = m - 1;
        int second = n - 1;

        for (int i = m + n - 1; i < m + n; i--) {
            if (second < 0){
                break;
            }else if (first < 0){
                num1[i] = num2[second];
                second--;
            }else if (num1[first] >= num2[second]){
                num1[i] = num1[first];
                first--;
            }else {
                num1[i] = num2[second];
                second--;
            }
        }

        return num1;
    }

}
4 移动零

leetcode 第283题

题目

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

个人解析

解析:
	设置两个指针,一个指针遍历数组,另一个指针停留在0的位置,每有一个值覆盖,指向0的指针后移,最后将指向0的指针和遍历指针之间置为0

代码

/**
 * @author zwj
 * @description 移动零
 * @location leetcode 第283题
 * @date 2022/7/22 - 17:51
 */
public class MoveZero {

    public static void main(String[] args) {
        int[] byArray = byNode(new int[]{0, 1, 0, 3, 12});
        for (int i : byArray) {
            System.out.println(i);
        }
    }

    /**
     * 数组移动覆盖,使用指针
     * @param nums
     * @return
     */
    public static int[] byNode(int[] nums){

        //创建指针
        int zero = 0;
        int next = 0;

        for (int i = 0; i < nums.length; i++, next++) {
            if (nums[next] != 0) {
                nums[zero] = nums[next];
                zero++;
            }
        }

        for (int i = zero; i < nums.length; i++) {
            nums[i] = 0;
        }
        return nums;
    }

}
5 找出所有数组中消失的数字

leetcode 第448题

题目

给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果.

分析

# 取反
在不需要额外空间的情况下,只能操作当前数组,时间复杂度为O(n)
因为nums[i]在在区间内,而且区间内数字从1开始,所以可以将数组中每个数字减一的下标位置取反
这样只有数组中缺失的数字减1的下标位置为正数。得到缺失的值


执行用时:5 ms, 在所有 Java 提交中击败了52.29%的用户
内存消耗:49.8 MB, 在所有 Java 提交中击败了16.93%的用户
通过测试用例:33 / 33

# 加值取模
和取反原理一样,将数组中的每个值减一位置的值加n,找下标时取模,最后小于n下标的就是缺失的

执行用时:3 ms, 在所有 Java 提交中击败了99.72%的用户
内存消耗:49.5 MB, 在所有 Java 提交中击败了40.10%的用户
通过测试用例:33 / 33

代码

/**
 * @author zwj
 * @description 找到数组中所有缺失的值
 * @location leetcode 第448题
 * @date 2022/7/25 - 9:48
 */
public class FindLoseNum {

    public static void main(String[] args) {
        int[] nums = {4,3,2,7,8,2,3,1};
//        find(nums);
        List<Integer> byMod = findByMod(nums);
        for (Integer integer : byMod) {
            System.out.println(integer);
        }
    }


    /**
     * 取反
     * @param nums
     */
    public static void find(int[] nums){
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] < 0){
                if (nums[~nums[i]] < 0){
                    continue;
                }
                nums[~nums[i]] = ~nums[~nums[i]] + 1;
            }else {
                if (nums[nums[i] - 1] < 0){
                    continue;
                }
                nums[nums[i] -1] = ~nums[nums[i] - 1] + 1;
            }
        }

        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] > 0){
                list.add(i + 1);
            }
        }

        for (Integer integer : list) {
            System.out.println(integer);
        }
    }

    public static List<Integer> findByMod(int[] nums){
        List<Integer> list = new ArrayList<>();
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            //对每个位置上的数取模减一获取下标
            int index = (nums[i] - 1) % n;
            //将下标上的值加数组长度
            nums[index] += n;
        }

        //判断小于n的值输出
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] <= n){
                list.add(i + 1);
            }
        }

        return list;
    }

}

链表

1 合并两个有序链表

leetcode 第21题

题目

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

分析

# 指针
准备一个结果节点,比较两个链表的每个节点的值,将小的值放入到结果节点,然后取出值的节点指针后移,直到为null
# 递归
递归链表的每个节点,进行比较,不断递归链表的next节点,然后将比较值接在next节点上,不需要创建新的链表空间

内存空间占用大

代码

/**
 * @author zwj
 * @description 合并有序链表
 * @location leetcode 第21题
 * @date 2022/7/25 - 10:46
 */
public class MergeNode {

    public static void main(String[] args) {

    }

    public static ListNode merge(ListNode list1, ListNode list2){
        //判断是否有链表为空
        if (list1 == null) return list2;
        if (list2 == null) return list1;

        //创建结果节点
        ListNode result = new ListNode(0);
        //创建p作为结果节点的指针
        ListNode p = result;
        while (list1 != null && list2 != null){
            if (list1.val < list2.val){
                p.next = list1;
                list1 = list1.next;
            }else {
                p.next = list2;
                list2 = list2.next;
            }
            p = p.next;
        }

		if (list1 == null) p.next = list2;
        if (list2 == null) p.next = list1;

        return result.next;
    }

    public static ListNode mergeByWhile(ListNode list1, ListNode list2){
        if (list1 == null)  return list2;
        if (list2 == null)  return list1;

        if (list1.val < list2.val){
            list1.next = mergeByWhile(list1.next, list2);
            return list1;
        }else {
            list2.next = mergeByWhile(list1, list2.next);
            return list2;
        }
    }
}
2 删除排序链表中的重复元素

leetcode 第83题

题目

给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 

分析

因为是有序链表,所以只需要遍历,与后一个节点比较,如果相同,直接指向后一个节点的下一个节点

# 还可以用递归

代码

/**
 * @author zwj
 * @description 删除有序链表中重复节点
 * @location leetcode 第83题
 * @date 2022/7/25 - 14:10
 */
public class DeleteDuplicates {

    public static void main(String[] args) {

    }

    public static ListNode delete(ListNode head){
        if (head == null) return null;
        //创建指针节点
        ListNode point = head;
        while(point.next != null){
            if (point.val == point.next.val){
                point.next = point.next.next;
            }else {
                point = point.next;
            }
        }
        return head;
    }

}
3 环形链表

leetcode 第141题 和 第142题

题目

# 141 简单
给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false# 142 中等
给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

分析

# 141
可以使用hash表,存储节点,如果出现相同节点就说明是环形,内存空间为O(n)

使用快慢指针,Floyd判圈算法
一个指针移动速度设为1,另一个指针速度设为2,当两个指针相遇时,就说明环形

# 142

代码

141

/**
 * @author zwj
 * @description 判断是否为环形链表
 * @location leetcode 第141题
 * @date 2022/7/25 - 14:42
 */
public class hasCircle {

    public static boolean justify(ListNode head){
        if (head == null) return false;
        //创建指针
        ListNode slowPoint = head;
        ListNode fastPoint = head;

        while (fastPoint.next != null && fastPoint.next.next != null){
            slowPoint = slowPoint.next;
            fastPoint = fastPoint.next.next;
            if (slowPoint == fastPoint){
                return true;
            }
        }
        return false;
    }
}

142

/**
 * @author zwj
 * @description 环形队列二 返回节点
 * @location leetcode 第142题
 * @date 2022/7/25 - 16:05
 */
public class ReturnCircleNode {

    public static ListNode returnNode(ListNode head){
        if (head == null) return null;
        if (head.next == null) return null;
        //创建指针
        ListNode slowPoint = head, fastPoint = head;
        ListNode pre = head;

        while (fastPoint != null){
            slowPoint = slowPoint.next;
            if (fastPoint.next != null){
                fastPoint = fastPoint.next.next;
            }else {
                return null;
            }
            if (fastPoint == slowPoint){
                while (pre != slowPoint){
                    pre = pre.next;
                    slowPoint = slowPoint.next;
                }

                return pre;
            }
        }
        return null;
    }

}
4 相交链表

leetcode 第160题

题目

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 
图示两个链表在节点 c1 开始相交:
相交后数据长度一致。
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 

分析

由题可知,如果链表相交,那么从交点到最后是一样的,所以,两个链表的关键就是交点前的长度。
设置两个指针,同时向后走,当一个指针走到头时,指针转移到另一个指针所在列表的头部,这样,另一个指针走完剩下的,这个指针就走到短链表相同的起始位置了,然后再将长链表的指针转到短链表开头,依次比对,找到交点

代码

/**
 * @author zwj
 * @description 相交链表
 * @location leetcode 第160题
 * @date 2022/7/25 - 17:16
 */
public class IntersectionNode {

    public static ListNode getIntersectionNode(ListNode head1, ListNode head2){
        if (head1 == null) return null;
        if (head2 == null) return null;
        //创建指针
        ListNode s = head1;
        ListNode p = head2;

        while (s != p){
            s = s == null ? head2 : s.next;
            p = p == null ? head1 : p.next;
        }
        return p;
    }

}
5 反转链表

leetcode 第206题

题目

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

分析

创建一个新的链表,将第一个节点指向新的链表,将后一个节点保存,重复操作

代码

/**
 * @author zwj
 * @description 反转链表
 * @location leetcode 第206题
 * @date 2022/7/25 - 17:47
 */
public class ReverseList {

    public static ListNode reverse(ListNode head){

        if (head == null) return null;
        //创建新链表
        ListNode newList = null;
        //创建当前链表指针
        ListNode curr = head;
        while (curr != null){
            //将当前链表循环赋值给临时链表
            ListNode point = curr.next;
            //当前链表节点指向新链表
            curr.next = newList;
            //将当前链表节点重新赋值给新链表
            newList = curr;
            //将临时链表中的节点重新赋给当前链表
            curr = point;
        }

        return newList;
    }
}
6 回文链表

leetcode 第234题

题目

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

分析

 解析一:
 	将链表遍历存储到数组中,使用头尾双指针遍历数组比较
 解析二:
 	使用快慢指针,找到中间节点,反转后一半链表,将慢指针复原到链表头部,进行比较,直到反转部分为空

代码

/**
 * @author zwj
 * @description 回文链表
 * @location leetcode 第234题
 * @date 2022/7/26 - 17:07
 */
public class Palindrome {

    /**
     * 使用数组保存链表数据进行比较
     * @param head
     * @return
     */
    public static boolean isPalindromeByArray(ListNode head){
        if (head == null) return false;
        int length = 0;
        int location = 0;
        ListNode curr = head;
        while(curr != null){
            length++;
            curr = curr.next;
        }
        int[] values = new int[length];
        while (head != null){
            values[location] = head.val;
            location++;
            head = head.next;
        }

        //创建首尾指针
        for (int pre = 0, tail = length - 1; pre < tail; pre++, tail--) {
            if (values[pre] != values[tail]){
                return false;
            }
        }
        return true;

    }

    /**
     * 通过反转后半部分链表进行比较
     * @param head
     * @return
     */
    public static boolean isPalindromeByReverse(ListNode head){
        if (head == null) return false;

        //找到反转链表的头部
        ListNode reverseHead = getMidNode(head);
        //反转链表
        ListNode reverseList = getReverseList(reverseHead);

        //对比节点数据
        while (reverseList != null){
            if (head.val != reverseList.val){
                return false;
            }
            head = head.next;
            reverseList = reverseList.next;
        }
        return true;

    }

    /**
     * 找到反转链表的头部
     * @param head
     * @return
     */
    public static ListNode getMidNode(ListNode head){
        //创建快慢指针
        ListNode slowPoint = head;
        ListNode fastPoint = head;
        /**
         * 如果next为空,则证明为偶数个,为空退出循环,那么slowPoint下一个就是反转链表的开头
         * 如果next.next为空,则证明为奇数个,那么slowPoint下一个就是反转链表的头
         */
        while (fastPoint.next != null && fastPoint.next.next != null){
            slowPoint = slowPoint.next;
            fastPoint = fastPoint.next.next;
        }

        return slowPoint.next;

    }

    /**
     * 反转链表
     * @param head
     * @return
     */
    public static ListNode getReverseList(ListNode head){
        //创建新链表
        ListNode newList = null;
        //创建原链表指针
        ListNode curr = head;
        //创建临时链表
        ListNode st;
        while (curr != null){
            st = curr.next;
            curr.next = newList;
            newList = curr;
            curr = st;
        }
        return newList;
    }

}

7 链表的中间节点

leecode 第876题

题目

给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。

分析

使用快慢指针,如果快指针.next为空则为偶数,如果快指针.next.next为空,则为奇数
奇数情况下,慢指针就是中间节点,偶数情况下,当前慢指针节点和.next节点是中间节点,返回第二个中间节点

综上,当为奇数时,返回慢指针节点,当为偶数时,返回慢指针的下一个节点
所以判断,如果快指针的下一个节点为空,直接返回慢指针,否则判断下下个节点为空,直接返回慢指针的下一个节点 

代码

/**
 * @author zwj
 * @description
 * @date 2022/7/27 - 10:10
 */
public class MidNode {

    public static ListNode getMidNode(ListNode head){
        if (head == null) return null;
        //创建快慢指针
        ListNode slowPoint = head;
        ListNode fastPoint = head;
        //判断快指针的下一个节点
        while (fastPoint.next != null){
            //判断快指针的下下一个节点
            if (fastPoint.next.next != null){
                slowPoint = slowPoint.next;
                fastPoint = fastPoint.next.next;
            }else {
                return slowPoint.next;
            }
        }
        return slowPoint;
    }
}
8 链表中倒数第K个节点

剑指offer 第22题

题目

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点

分析

# 解析
	遍历链表,确认链表个数,length - k找到节点

代码

/**
 * @author zwj
 * @description
 * @date 2022/7/27 - 10:36
 */
public class FromEndKNode {

    public static ListNode getEndKNode(ListNode head, int k){
        if (head == null) return null;
        int length = 0;
        //创建当前链表指针
        ListNode point = head;
        while (point != null){
            length++;
            point = point.next;
        }

        for (int i = 0; i < length - k; i++) {
            head = head.next;
        }
        return head;
    }

}

栈和队列

概念

栈:后进先出 LIFO
队列:先进先出	FIFO
1 用栈实现队列

leetcode 第232题

题目

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):

实现 MyQueue 类:
void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false

分析

使用两个栈,一个作为输入栈,一个作为输出栈,每次使用输出栈时,判断输出栈是否为空,如果为空,从输入栈导出一次数据

代码

package leetcode.stack;

import java.util.Iterator;
import java.util.Stack;

/**
 * @author zwj
 * @description 用栈实现队列
 * @location leetcode 第232题
 * @date 2022/7/28 - 15:16
 */
public class MyQueue {

    private static Stack<Integer> inStack;
    private static Stack<Integer> outStack;

    public MyQueue() {
        inStack = new Stack<>();
        outStack = new Stack<>();
    }

    public void push(int x) {
        inStack.push(x);
    }

    public int pop() {
        if (outStack.empty()){
            outPush();
        }
        return outStack.pop();
    }

    public int peek() {
        if (outStack.empty()){
            outPush();
        }
        return outStack.peek();
    }

    public boolean empty() {
        if (inStack.empty() && outStack.empty()){
            return true;
        }else {
            return false;
        }
    }

    public static void outPush(){
        Iterator<Integer> iterator = inStack.iterator();
        while (iterator.hasNext()){
            outStack.push(inStack.pop());
        }
    }

}
2 字符串解码

leetcode 第394题

题目

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入

示例 1:

输入:s = "3[a]2[bc]"
输出:"aaabcbc"
示例 2:

输入:s = "3[a2[c]]"
输出:"accaccacc"
示例 3:

输入:s = "2[abc]3[cd]ef"
输出:"abcabccdcdcdef"
示例 4:

输入:s = "abc3[cd]xyz"
输出:"abccdcdcdxyz"

分析

由题可知,每一对中括号内的值重复出现k次
所以,每遇到闭中括号,向前找到对应的开中括号,然后再找到开中括号前的数字,就可以

所以适合使用栈
遇到闭中括号之前一直压栈,直到闭中括号出现,出栈,放入队列,队列先进先出,使用springbuilder组成字符串,然后再出栈一次将数字出栈,循环数字后,将结果压栈,继续循环

注意:数字可能多位数,所以在遇到数字后,需要多次判断
package leetcode.stackandqueue.demo015;

import java.util.*;

/**
 * @author zwj
 * @description 字符串解码
 * @location leetcode 第394题
 * @date 2022/7/28 - 16:03
 */
public class DecodeString {

    public static void main(String[] args) {
        String s = "3[a]2[bc]";
        decodeString(s);
    }

    public static String decodeString(String s){

        //创建栈
        Stack<String> stack = new Stack<>();
        for (int i = 0; i < s.length();) {
            char cur = s.charAt(i);
            //判断当前字符是否为数字
            if (Character.isDigit(cur)){
                StringBuilder number = new StringBuilder();
                //循环判断数字位数
                while (Character.isDigit(s.charAt(i))){
                    number.append(s.charAt(i++));
                }
                //数字入栈
                stack.push(number.toString());
            }else if (Character.isLetter(cur) || cur == '['){
                //字母或[入栈
                stack.push(String.valueOf(s.charAt(i++)));
            }else {
                ++i;
                //创建新栈
                Stack<String> component = new Stack<>();
                //出栈
                while (true){
                    String pop = stack.pop();
                    if (!pop.equals("[")){
                        component.push(pop);
                    }else {
                        break;
                    }
                }
                //次数出栈
                int times = Integer.parseInt(stack.pop());
                //获取字符串对象
                String o = getString(component);
                //创建builder
                StringBuilder builder = new StringBuilder();
                while (times-- > 0){
                    builder.append(o);
                }
                //入栈
                stack.push(builder.toString());
            }
        }
        Collections.reverse(stack);
        return getString(stack);
    }

    private static String getString(Stack<String> stack) {
        StringBuilder sb = new StringBuilder();
        while (!stack.empty()){
            sb.append(stack.pop());
        }
        return sb.toString();
    }

}

概念

树是n(n>=0)个结点的有限集。n=0时称为空树
在任意一棵非空树中:
	1)有且只有一个特定的称为根(root)的结点
	2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2...,Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)
1.树的度
	节点孩子的个数是节点的度,树的度为节点中最大的度
2.结点间关系
	父子结点,兄弟结点,祖先结点
3.树的深度
	树的层数

树的表示

双亲表示法

编号 data parent
0 A -1
1 B 0
2 C 0
3 D 1

孩子表示法

data degree(度) child1 child2 child3 childn

二叉树

概念

二叉树每个结点最多有两个子结点

特殊二叉树

1.斜树
2.满二叉树: 所有的结点都有左右子结点且所有叶子结点都在同一层,非叶子结点的度都是2
	结点个数等于  2的深度次方-1
3.完全二叉树:和满二叉树按一样的顺序编号,编号位置一致
	除了叶子结点,其他节点必须和满二叉树一致
二叉树的中序遍历

leetcode 第94题

题目

给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。

分析

中序遍历: 左根右

循环遍历左子树,一直到左叶子结点
但是由于是左根右,所以左结点遍历后,需要返回一步到根结点
所以,使用栈将每个根结点压栈,每退回一步,栈出栈一个结点。
根结点出栈后,还要看右结点,遍历右结点有没有左子结点

递归
package leetcode.tree.demo016;

import leetcode.tree.node.TreeNode;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
 * @author zwj
 * @description 中序遍历二叉树
 * @location leetcode 第94题
 * @date 2022/7/29 - 14:19
 */
public class MidOrderTraversal {

    public static List<Integer> MidOrder(TreeNode root){
        //创建list
        List<Integer> list = new ArrayList<>();
        if (root == null) return list;
        //创建栈存储根节点
        Stack<TreeNode> stack = new Stack<>();
        //循环判断条件,root不等于null或stack中有结点
        while (root != null || !stack.isEmpty()){
            //循环遍历二叉树左结点
            while (root != null){
                //将根节点存入栈
                stack.push(root);
                //遍历左节点
                root = root.left;
            }
            //出循环,当前root为空,根节点就是最左叶子结点,获取当前左结点的根节点
            root = stack.pop();
            //获取当前最左叶子结点数据
            list.add(root.val);
            //将当前结点转为右子结点,重新遍历左节点,如果右结点为空,会跳过循环再次出栈,不为空,则找到最左叶子结点,重复操作
            root = root.right;
        }

        //当栈内为空时,root为空,说明已经没有节点,输出list
        return list;

    }
    
    
    //不能用static,因为测试用例有多个,要不然需要清空list
    public List<Integer> list = new ArrayList<>();
    /**
     * 递归
     * @param root
     * @return
     */
    public List<Integer> inorderTraversal(TreeNode root){
        if (root == null) return list;
        list = inorderTraversal(root.left);
        list.add(root.val);
        list = inorderTraversal(root.right);
        return list;
    }
}

二叉树前序遍历

leetcode 第144题

题目

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

分析

前序遍历: 根左右

先访问根节点,在访问左右结点
同中序遍历一样遍历,只是添加顺序做一下改变,先保存根数据
从根节点开始一直向左遍历,同时添加数据,到叶子节点时,pop根结点,指向右结点,继续遍历左节点 

代码

package leetcode.tree.demo016;

import leetcode.tree.node.TreeNode;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
 * @author zwj
 * @description 前序遍历
 * @location leetcode 第144题
 * @date 2022/7/29 - 15:43
 */
public class BeforeOrderTraversal {

    public static List<Integer> beforeOrder(TreeNode root){
        List<Integer> list = new ArrayList<>();
        if (root == null) return list;
        //创建栈存储根结点
        Stack<TreeNode> stack = new Stack<>();
        while (root != null || !stack.empty()){
            while(root != null){
                //直接将值存储
                list.add(root.val);
                //将根节点存入栈
                stack.push(root);
                //左节点遍历
                root = root.left;
            }
            //将根节点出栈
            root = stack.pop();
            //开始遍历右结点
            root = root.right;
        }
        //输出list
        return list;

    }


    public List<Integer> list = new ArrayList<>();
    /**
     * 递归
     * @param root
     * @return
     */
    public List<Integer> preorderTraversal(TreeNode root){
        if (root == null) return list;
        list.add(root.val);
        list = preorderTraversal(root.left);
        list = preorderTraversal(root.right);
        return list;
    }

}
二叉树后序遍历

leetcode 第145题

题目

给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历 。

分析

后序遍历:左右根

一直向左子结点遍历,直到左叶子结点,栈存储根节点,拿到子叶子结点后,pop根节点,在push根结点,向右结点遍历
因为左右根,所以,遍历右结点时,需要做记录,如果右结点为空,直接到根,如果右结点已经拿过值,直接返回根
package leetcode.tree.demo016;

import leetcode.tree.node.TreeNode;

import java.util.*;

/**
 * @author zwj
 * @description 后序遍历
 * @location leetcode 第145题
 * @date 2022/7/29 - 16:02
 */
public class AfterOrderTraversal {

    public static void main(String[] args) {
        TreeNode root = new TreeNode(3);
        TreeNode one = new TreeNode(1);
        TreeNode two = new TreeNode(2, null, null);
        root.left = one;
        root.right = null;
        one.left = null;
        one.right = two;
        afterOrder(root);
    }

    public static List<Integer> afterOrder(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if (root == null) return list;
        //创建栈
        Stack<TreeNode> stack = new Stack<>();
        Stack<TreeNode> rootStack = new Stack<>();
        TreeNode st;
        while (root != null || !stack.empty() || !rootStack.empty()){
            while (root != null){
                stack.push(root);
                root = root.left;
            }
            if (!stack.empty()){
                root = stack.pop();
                rootStack.push(root);
                root = root.right;
            }
            if (root == null){
                list.add(rootStack.pop().val);
            }else {
                TreeNode pop = rootStack.pop();
                pop.left = null;
                pop.right = null;
                stack.push(pop);
            }
        }
        return list;

    }

    public static List<Integer> afterOrderTwo(TreeNode root){
        List<Integer> res = new ArrayList<Integer>();
        if (root == null) {
            return res;
        }

        //创建栈
        Deque<TreeNode> stack = new LinkedList<TreeNode>();
        //创建结点
        TreeNode prev = null;
        while (root != null || !stack.isEmpty()) {
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            //判断结点为空或者结点的右结点已经出现过
            // root.right = prev这个条件,就是为了放置重复遍历结点,因为每个push到栈的结点的子节点可能重复
            if (root.right == null || root.right == prev) {
                //添加数据
                res.add(root.val);
                //将当前结点赋给prev
                prev = root;
                //使用root = null 条件释放栈中的结点
                root = null;
            } else {
                //当前结点还有子节点,将当前结点入栈
                stack.push(root);
                //遍历子节点
                root = root.right;
            }
        }
        return res;
    }
    
    public List<Integer> list = new ArrayList<>();
    /**
     * 递归
     * @param root
     * @return
     */
    public List<Integer> postorderTraversal(TreeNode root){
        if (root == null) return list;
        list = postorderTraversal(root.left);
        list = postorderTraversal(root.right);
        list.add(root.val);
        return list;
    }

}

对称二叉树

leetcode 第101题

题目

给你一个二叉树的根节点 root , 检查它是否轴对称。

分析

将root结点下的左右结点分成两个根节点,遍历结点并对比,采用递归

代码

/**
 * @author zwj
 * @description 对称二叉树
 * @location leetcode 第101题
 * @date 2022/7/29 - 17:55
 */
public class SymmetricTree {
    
	public boolean isSymmetric(TreeNode left, TreeNode right){
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(left);
        queue.offer(right);
        while (!queue.isEmpty()){
            left = queue.poll();
            right = queue.poll();
            if (left == null && right == null){
                continue;
            }
            if (left == null || right == null || left.val != right.val){
                return false;
            }
            queue.offer(left.left);
            queue.offer(right.right);

            queue.offer(left.right);
            queue.offer(right.left);
        }
        return true;
    }

    //递归
    public static boolean isSymmetric(TreeNode left, TreeNode right){
        if (left == null && right == null) return true;
        if (left == null || right == null) return false;
        boolean a = isSymmetric(left.left, right.right);
        boolean b = isSymmetric(left.right, right.left);
        if (a == true && b == true){
            if (left.val == right.val){
                return true;
            }
        }
        return false;
    }
    
    
    
}
二叉树的最大深度

leetcode第104题

题目

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

分析

使用递归

如果我们知道了左子树和右子树的最大深度 l 和 r,
那么该二叉树的最大深度即为

max(l,r)+1

而左子树和右子树的最大深度又可以以同样的方式进行计算。
因此我们可以用「深度优先搜索」的方法来计算二叉树的最大深度。
具体而言,在计算当前二叉树的最大深度时,
可以先递归计算出其左子树和右子树的最大深度,
然后在 O(1)O(1) 时间内计算出当前二叉树的最大深度。
递归在访问到空节点时退出。

代码

/**
 * @author zwj
 * @description 二叉树最大深度
 * @date 2022/8/3 - 9:53
 */
public class MaxDepthTree {

    public int maxDepth(TreeNode root) {
        if (root == null) return 0;
        return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
    }

}
平衡二叉树

leetcode 第110题

题目

给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1

分析

根据深度递归遍历,可以知道,递归遍历是对每一个结点的左右结点进行遍历,
所以可以利用这一点,对每一个结点的左右结点遍历,直到为空,
判断每一个结点的左右节点的高度差,返回值

代码

/**
 * @author zwj
 * @description 平衡二叉树
 * @location leetcode 第110题
 * @date 2022/8/3 - 10:12
 */
public class BalancedTree {

    public boolean isBalanced(TreeNode root){
        if (root == null) return true;
        return depth(root) != -1;
    }


    public int depth(TreeNode root){
        if (root == null) return 0;
        int left = depth(root.left);
        int right = depth(root.right);
        if (Math.abs(left - right) > 1 || left == -1 || right == -1){
            return -1;
        }
        return Math.max(left, right) + 1;
    }

}
翻转二叉树

leetcode 第226题

题目

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点

分析

由题可知,做法就是循环迭代将左右结点调换

代码

/**
 * @author zwj
 * @description 翻转二叉树
 * @location leetcode 第226题
 * @date 2022/8/3 - 10:34
 */
public class ReverseTree {

    public TreeNode invertTree(TreeNode root) {
        if (root == null) return null;
        //对每个结点施加操作
        invertTree(root.left);
        invertTree(root.right);
        //翻转规则
        TreeNode st = root.left;
        root.left = root.right;
        root.right = st;
        return root;
    }
}

你可能感兴趣的:(笔记,链表,数据结构,算法)