当前位置: 首页 > article >正文

优选算法 - 4 ( 链表 哈希表 字符串 9000 字详解 )

一:链表

1.1 链表常用技巧和操作总结

在这里插入图片描述
在这里插入图片描述

1.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 addTwoNumbers(ListNode l1, ListNode l2) {
        // 先用 cur1 指向 l1 的头节点,用 cur2 指向 l2 的头节点 -- 链表的名字代表着链表头部节点的地址
        // 因为要记录节点的信息,比如说 val 和 next ,所以 cur1 和 cur2 都用 ListNode 节点类型
        ListNode cur1 = l1, cur2 = l2;
        // 接着创建一个虚拟节点 newHead,当两个链表相加后延长这个节点,并把值存储在对应的节点中
        ListNode newHead = new ListNode(0);
        // 用 prev 标记一下 newHead  延长后的尾节点
        ListNode prev = newHead;
        // 用 t 临时存储两个节点相加的结果
        int t = 0;
 
        // 当 cur1 或 cur2 不为空并且 t 没有剩余值时就一直循环,t 不为空是为了处理两个数相加进位后剩下的数字
        while(cur1 != null || cur2 != null || t != 0){
            if(cur1 != null){
                t += cur1.val;
                cur1 = cur1.next;
            }

            if(cur2 != null){
                t += cur2.val;
                cur2 = cur2.next;
            }

            // 当 t 累加上两个节点的值后,把 t 的个位放入 newHead 延长的节点中,接着让 prev 继续指向尾节点
            prev.next = new ListNode(t % 10);
            prev = prev.next;

            // 接着让 t 的十位继续加
            t /= 10;
        }

        // 返回结果链表的第一个有效节点,我们要跳过虚拟头节点
        return newHead.next;
    }
}

在这里插入图片描述

1.3 两两交换链表中的结点

题目链接:两两交换链表中的结点

在这里插入图片描述
在这里插入图片描述

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode swapPairs(ListNode head) {
        // 先处理链表为空和链表元素为一的情况,这种情况链表无需交换
        if(head == null || head.next == null) return head;

        // 接着处理正常的情况,先创建一个虚拟头节点 newHead ,方便交换,不用处理太多的细节
        ListNode newHead = new ListNode(0);
        //接着把 newHead 当作 head 的头节点
        newHead.next = head;

        // 初始化四个指针:
        // `prev` 用于指向当前要交换的节点的前一个节点
        // `cur` 用于指向当前要交换的第一个节点
        // `next` 用于指向当前要交换的第二个节点
        // `nnext` 用于指向当前要交换的第二个节点后一个节点
        ListNode prev = newHead, cur = prev.next, next = cur.next, nnext = next.next;
        while(cur != null && next != null){
            prev.next = next;
            next.next = cur;
            cur.next = nnext;

            //交换完两个数字后更新四个指针,让接下来的节点继续交换
            prev = cur;
            cur = nnext;
            if (cur != null) next = cur.next; // `next` 移到下一对的第二个节点(如果存在)
            if (next != null) nnext = next.next; // `nnext` 移到下一对的第一个节点的后一个节点(如果存在)
        }

        //循环结束后返回 head ,当然,要去掉虚拟节点
        return newHead.next;
    }
}

在这里插入图片描述

1.4 重排链表

题目链接:重排链表
在这里插入图片描述

在这里插入图片描述

/**
 * 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 void reorderList(ListNode head) {
        // 先处理链表为空和链表只有一个节点以及两个节点的情况,这些情况下,链表不需要重排
        if(head == null  || head.next == null || head.next.next == null) return;

        //接下来就处理正常情况了,首先先找到链表的中间节点,这里用快慢双指针的方法
        ListNode slow = head, fast = head;
        while(fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
        }

        //循环过后 slow 就是 head 的中间节点,接着我们把 head 分为两个部分 [0, slow],[slow.next, n]
        //我们用 head2 存储第二部分的链表,利用头插法对链表逆序
        ListNode head2 = new ListNode(0);
        //用 cur 标记第二部分链表的首元素
        ListNode cur = slow.next;
        // 接着以 slow 为界将链表前后部分分离
        slow.next = null;                 
        while(cur != null){
            // 因为我们头插 cur 时,cur 与原来的 cur.next 会断开,所以我们用 next 先记录一下这个节点,方便进行循环
            ListNode next = cur.next;
            cur.next = head2.next;
            head2.next = cur;
            cur = next;
        }

        // 接着我们就可以开始合并 head 和 head2 了
         // 用 ret 存储合并后的链表,并用 prev 标记 ret 的尾节点,用 cur1 标记 head 的头节点, cur2 标记 head2 的头节点
        ListNode ret = new ListNode(0);
        ListNode cur1 = head, cur2 = head2.next, prev = ret;

        // 因为 head 的长度一定是大于 head2 的,并且我们合并数组时是分别取出一个的,所以我们只需要判断 head 不为空
        while(cur1 != null){
            //先把 head 中的元素添加进 ret 中
            prev.next = cur1;
            prev = prev.next;
            cur1 = cur1.next;

            //因为 head2 的长度小于 head ,所以我们要判断一下 cur2 是否为空
            if(cur2 != null){
                prev.next = cur2;
                prev = prev.next;
                cur2 = cur2.next;
            }
        }
        
         // 链表重排完成,无需返回值,因为操作在原链表上完成
        
    }
}

在这里插入图片描述

1.5 合并 K 个升序链表

题目链接:合并 K 个升序链表

在这里插入图片描述
在这里插入图片描述

/**
 * 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 mergeKLists(ListNode[] lists) {
        // 创建一个小根堆(优先队列),根据节点值进行排序
        // lambda 表达式:(v1, v2) -> v1.val - v2.val 用于实现按节点值升序排列
        PriorityQueue<ListNode> heap = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);

        // 接着将所有链表的头结点加入到小根堆中,注意!并不是取的链表所有的节点
        for(ListNode Li : lists)
            if(Li != null) heap.offer(Li);
        
        // 接着创建一个 ret 存储合并后的升序链表,用 prev 指向 ret 的尾节点
        ListNode ret = new ListNode(0);
        ListNode prev = ret;

        // 接着就把小堆里的元素依次放入 ret 中
        while(heap.isEmpty() == false){
            //尾插到 prev 后面,并更新 prev 的指向
            ListNode t = heap.poll();
            prev.next = t;
            prev = t;

            // 如果堆顶节点 t 有下一个节点,将其加入堆
            if (t.next != null) {
                heap.offer(t.next); // 把 t 的下一个节点加入堆中
            }
        }

        // 返回合并后的链表的头节点(去掉虚拟头节点 ret)
        return ret.next;
    }
}

在这里插入图片描述

1.6 K 个一组翻转链表

题目链接:K 个一组翻转链表

在这里插入图片描述

在这里插入图片描述

/**
 * 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 reverseKGroup(ListNode head, int k) {
        // 先获取链表需要反转的次数 n 
        int n = 0;
        ListNode cur = head; // 指针初始化为链表头节点
        while(cur != null){
            cur = cur.next;
            n++;
        }
        n /= k; // 此时 n 就存储了数组要反转的次数了

        // 接着创建一个虚拟节点 newHead ,用于存储翻转后的链表
        ListNode newHead = new ListNode(0);
        // 接着用 prev 标记要插入位置的前一个节点
        ListNode prev = newHead;
        cur = head; // 重置一下 cur ,因为前面遍历链表时修改了 cur

        // 先处理 n 次需要翻转 k 个元素的情况
        for(int i = 0; i < n; i++){
            ListNode tmp = cur; // 先用 tmp 临时存储一下第一个插入的节点
            for(int j = 0; j < k; j++){
                ListNode next = cur.next; // 先用 next 存储一下插入节点的下一个节点
                cur.next = prev.next;
                prev.next = cur;
                cur = next;
            }
            // 当一组结束完后要重新更新一下 prev ,让下一轮循环以 prev 为基准进行头插
            prev = tmp;
        }

        // 接着处理一下剩余的元素
        prev.next = cur;

        // 最后返回 newHead ,要跳过虚拟节点
        return newHead.next;
    }
}

在这里插入图片描述

二:哈希表

2.1 两数之和

题目链接:两数之和
在这里插入图片描述

在这里插入图片描述

class Solution {
    public int[] twoSum(int[] nums, int target) {
        // 键:数组元素的值,值:该元素在数组中的索引
        Map<Integer, Integer> hash = new HashMap<>(); // <nums[i], i>
        
        // 接着开始遍历数组
        for(int i = 0; i < nums.length; i++){
            int x = target - nums[i];
            if(hash.containsKey(x)) return new int[]{i, hash.get(x)};
            else hash.put(nums[i], i);
        }

        // 如果找不到返回 -1 和 -1 
        return new int[]{-1, -1};
    }
}

在这里插入图片描述

2.2 判断是否互为字符重排

题目链接:判断是否互为字符重排

在这里插入图片描述
在这里插入图片描述

class Solution {
    public boolean CheckPermutation(String s1, String s2) {
        // 先处理特殊情况,当两个字符串的长度不相同时,一定不是重排字符串2
        if(s1.length() != s2.length()) return false;

        // 接下来处理正常的情况,我们用大小为 26 的数组模拟哈尼表,用于存储 'a' - 'z' 字符
        int[] hash = new int[26];
        // 先遍历第一个字符串 s1 ,遇到一个字符让对应的下标加加
        for(int i = 0; i < s1.length(); i++){
            hash[s1.charAt(i) - 'a']++;
        }
        // 接着处理第二个字符串 s2 ,当遇到相同的字符时,让对应下标减减
        for(int i = 0; i < s2.length(); i++){
            hash[s2.charAt(i) - 'a']--;
            // 此时如果有某一位出现负数了。那么直接返回 false
            if(hash[s2.charAt(i) - 'a'] < 0) return false;
        }

        return true;
    }
}

在这里插入图片描述

2.3 存在重复元素

题目链接:存在重复元素

在这里插入图片描述
在这里插入图片描述

class Solution {
    public boolean containsDuplicate(int[] nums) {
        // 此处我们用 set 存储数组中的元素
        Set<Integer> hash = new HashSet<>();

        // 接着开始遍历数组
        for(int x : nums){
            if(hash.contains(x)) return true;
            else hash.add(x);
        }

        return false;
    }
}

在这里插入图片描述

2.4 存在重复元素 II

题目链接:存在重复元素 II

在这里插入图片描述
在这里插入图片描述

class Solution {
    public boolean containsNearbyDuplicate(int[] nums, int k) {
        // 创建一个哈希表,用于记录数组中的每个元素及其对应的索引
        // 键:数组中的元素值,值:该元素的最新索引
        Map<Integer, Integer> hash = new HashMap<>();

        // 接着开始遍历数组
        for(int i = 0; i < nums.length; i++){
            // 先判断一下这个数之前是否存在有相同的元素
            if(hash.containsKey(nums[i])){
                if(i - hash.get(nums[i]) <= k) return true;
            }

            hash.put(nums[i], i);
        }

        return false;
    }
}

在这里插入图片描述

2.5 字母异位词分组

题目链接:字母异位词分组

在这里插入图片描述

在这里插入图片描述

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        // 创建一个哈希表,用于将字母异位词分组
        // 键:经过排序的字符串(字母异位词的特征值)
        // 值:与该特征值对应的所有字母异位词的列表
        Map<String, List<String>> hash = new HashMap<>();

        // 1. 遍历字符串数组,将字母异位词分组
        for (String s : strs) {
            // 将当前字符串转换为字符数组
            char[] tmp = s.toCharArray();
            // 对字符数组进行排序,使异位词具有相同的排序结果
            Arrays.sort(tmp);
            // 将排序后的字符数组转换为字符串,作为分组的键
            String key = new String(tmp);

            // 如果哈希表中不存在该键,初始化一个新的列表作为值
            if (!hash.containsKey(key)) hash.put(key, new ArrayList<>());
            

            // 将当前字符串添加到对应键的列表中
            hash.get(key).add(s);
        }

        // 2. 提取结果
        // 将哈希表中所有的值(分组列表)转换为一个列表返回
        return new ArrayList<>(hash.values());
    }
}

在这里插入图片描述

三:字符串

3.1 最长公共前缀

题目链接:最长公共前缀

在这里插入图片描述
在这里插入图片描述

class Solution {
    public String longestCommonPrefix(String[] strs) {
        // 先处理一下特殊的情况,当字符串为 null 以及 strs 中的字符串个数为 0 ,这种情况下我们直接返回空字符串
        if(strs == null || strs.length == 0) return "";

        // 如果不是的话就处理正常的情况,我们以 strs 中的第一个字符串为基准,当前缀的长度大于等于 strs 中第一个字符串的长度时,循环停止
        int index = 0; // 用 index 记录最后第一个字符串的最长公共前缀的索引
        while(true){
            if(index >= strs[0].length()) break;

            char currentChar = strs[0].charAt(index); // 先获取第一个字符串的当前位置的字符
            // 接着开始遍历第二个及以下的字符串
            for(int i = 1; i < strs.length; i++){
                // 同样的,index 的长度也不能超过其他字符串的长度
                if(index >= strs[i].length() || strs[i].charAt(index) != currentChar) return strs[0].substring(0,index);
            }

            // 如果 index 没有超过长度并且字符相同的话,让 index 继续加加,继续比较下一个字符
            index++;
        }

        return strs[0].substring(0, index);
    }
}

在这里插入图片描述

3.2 最长回文子串

题目链接:最长回文子串

在这里插入图片描述
在这里插入图片描述

class Solution {
    public String longestPalindrome(String s) {
        // 用 begin 记录最长回文子串的起始位置,len 记录最长回文子串的长度,n 存储字符串 s 的长度
        int begin = 0, len = 0, n = s.length();
        
        // 接着遍历字符串中的每一个元素
        for(int i = 0; i < n; i++){
            // 先处理奇数的情况
            int left = i, right = i;
            while(left >= 0 && right < n && s.charAt(left) == s.charAt(right)){left--; right++;}
            // 扩展结束后看一下要不要更新 begin 和 len
            if(right - left - 1 > len){
                begin = left + 1; // 因为 left 和 right 都越过了回文子串的范围,他们是用完后再加加的
                len  = right - left - 1;
            } 

            // 接着处理偶数的情况2
            left = i; right = i + 1;
            while(left >= 0 && right < n && s.charAt(left) == s.charAt(right)){left--; right++;}
            if(right - left - 1 > len){
                begin = left + 1; // 因为 left 和 right 都越过了回文子串的范围,他们是用完后再加加的
                len  = right - left - 1;
            } 
        }

        // 循环结束后根据 len 和 begin 来截取字符串
        return s.substring(begin, begin + len);
    }
}

在这里插入图片描述

3.3 二进制求和

题目链接:二进制求和

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class Solution {
    public String addBinary(String a, String b) {
        // 因为要频繁的对字符串进行操作,因此我们使用 StringBuffer 来存储结果字符串
        StringBuffer ret = new StringBuffer();
        // 接着用 cur1 指向字符串 a 的末尾, cur2 指向字符串 b 的末尾,t 用来累加结果
        int cur1 = a.length() - 1, cur2 = b.length() - 1, t = 0;
        // 接着就可以开始从低位相加了
        while(cur1 >= 0 || cur2 >= 0 || t != 0){
            // 因为循环的条件是或,因此我们还要再循环内再判断一次,防止 cur1 和 cur2 越界,- '0' 是为了把字符转换为数字
            if(cur1 >= 0) t += a.charAt(cur1--) - '0';
            if(cur2 >= 0) t += b.charAt(cur2--) - '0';

            // 接着取出 t 的当前位,进位保留, + '0' 是把数字转换成字符
            ret.append((char)('0' + (t % 2)));
            t /= 2;
        }

        // 循环结束后 ret 就存储着求和的值,但注意,这个值的逆序的
        ret.reverse();

        // 最后把 ret 转换为 String 返回
        return ret.toString();
    }
}

在这里插入图片描述

3.4 字符串相乘

题目链接:字符串相乘
在这里插入图片描述
在这里插入图片描述

class Solution
{
    public String multiply(String num1, String num2)
    {
        // 获取两个数字的长度
        int m = num1.length(), n = num2.length();
        
        // 将数字字符串反转并转换为字符数组,方便逐位计算
        char[] n1 = new StringBuffer(num1).reverse().toString().toCharArray();
        char[] n2 = new StringBuffer(num2).reverse().toString().toCharArray();
        
        // 创建一个临时数组,用于存储无进位的乘积结果
        int[] tmp = new int[m + n - 1]; // 长度为m+n-1,因为每一位乘积最多扩展到m+n-1位
        
        // 1. ⽆进位相乘后累加
        for(int i = 0; i < m; i++) { // 遍历num1的每一位
            for(int j = 0; j < n; j++) { // 遍历num2的每一位
                // 把两位数字的乘积加到对应位置(i+j)
                tmp[i + j] += (n1[i] - '0') * (n2[j] - '0');
            }
        }
        
        // 2. 处理进位
        int cur = 0; // 当前处理的位置
        int t = 0; // 进位值
        StringBuffer ret = new StringBuffer(); // 用于存储最终的结果

        // 遍历tmp数组,将无进位的结果转化为正确的数字
        while(cur < m + n - 1 || t != 0) // 遍历到tmp的末尾或者有进位需要处理
        {
            if(cur < m + n - 1) // 如果当前位还在tmp数组范围内
                t += tmp[cur++]; // 加上当前位的值,并移动到下一位
            
            // 当前位的数字是t % 10,进位是t / 10
            ret.append((char)(t % 10 + '0')); // 保存当前位的数字(转换为字符后添加到结果中)
            t /= 10; // 更新进位值
        }

        // 3. 去除结果中的多余前导零
        while(ret.length() > 1 && ret.charAt(ret.length() - 1) == '0') {
            ret.deleteCharAt((ret.length() - 1)); // 删除结果最后的0,直到结果长度大于1
        }

        // 结果需要再反转一次,变为正序返回
        return ret.reverse().toString();
    }
}

在这里插入图片描述


http://www.kler.cn/a/402242.html

相关文章:

  • 【Android】线程池的解析
  • 【西瓜书】机器学习的模型评估
  • Spark RDD 中的 repartition 和 coalesce 是两种常用的分区调整算子的异同点
  • MMaction2:常见问题解答
  • 【AI+教育】一些记录@2024.11.16
  • 从0开始学习机器学习--Day26--聚类算法
  • vxe-table 表格多选启用快捷选择功能,鼠标滑动范围选择功能
  • 【Java系列】优化spring boot项目的启动加载,减少启动时的资源耗费的几种方案
  • 【MySQL-3】表的约束
  • 接口文档判断返回 List 还是 Array
  • 《Django 5 By Example》阅读笔记:p165-p210
  • [JavaWeb]微头条项目
  • UE5开发记录-如何解决播放完开始动画Sequence然后再显示UI?
  • SpringBoot服务多环境配置
  • 0017__多播,IP_MULTICAST_TTL,IP_ADD_MEMBERSHIP,IP_MULTICAST_IF,IP_DROP_MEMBERSHIP
  • 【主机游戏】犯罪现场清理工
  • 基于SSM的农家乐管理系统+论文示例参考
  • 【Redis】Redis实现的消息队列
  • Vue3插槽v-slot使用方式
  • kubesphere问题处理:devops