LeetCode - 字符串与链表

一. 字符串

1. 字符串比较

例题 242,有效的字母异位词。给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。

  • 第一种方式排序后判断是否相等;
  • 第二种,哈希表存储字母出现频次。由于字母在26之内,因此可以通过26位数组代替哈希表。
public boolean isAnagram(String s, String t) {
        if (s.length() != t.length()) {
            return false;
        }
        int[] arr = new int[26];
        for (int i = 0; i < s.length(); i++) {
            arr[s.charAt(i) - 'a'] ++;
            arr[t.charAt(i) - 'a'] --;
        }
        for (int i = 0; i < 26; i++) {
            if (arr[i] != 0) {
                return false;
            }
        }
        return true;
    }

例题205,同构字符串。给定两个字符串 s 和 t ,判断它们是否是同构的。如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。

  • 这道题的意思是 s 可以满足要求映射为 t,t 也可以满足要求映射到 s。但是 s 映射到 t 与 t 映射到 s 的映射关系可以不一致;
  • 可以建立两个哈希表,分别存储 s 映射到 t 与 t 映射到 s 的映射关系。如果遍历过程中发现映射关系不满足对应要求,则不是同构的。(由于这里 s、t 不只是小写字母,因此不能用26位数组)
  • 还有一种方法,如果两个字符串各个位置上的字符第一次出现的位置相等,则是同构的。
public boolean isIsomorphic(String s, String t) {
        if (s.length() != t.length()) {
            return false;
        }
        for (int i = 0; i < s.length(); i++) {
            if (s.indexOf(s.charAt(i)) != t.indexOf(t.charAt(i))) {
                return false;
            }
        }
        return true;
    }

例题 647. 回文子串。给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。回文字符串 是正着读和倒过来读一样的字符串。子字符串 是字符串中的由连续字符组成的一个序列。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

  • 这道题不止要判断一个字符串是否是回文串,还需要找到所有回文子串;这里可以遍历其所有子串,判断其是否是回文串。注意遍历时子串需要从短到长(可以外层倒序,内层正序)。
  • 动态规划,dp[i][j]表示s[i:j]子串是不是回文子串。
public int countSubstrings(String s) {
        int len = s.length();
        boolean[][] dp = new boolean[len][len];
        int res = 0;
        for (int k = 0; k < len; k++) {
            for (int i = 0; i < len-k; i++) {
                //如果两端的字符相等,则判断中间的字符是否是回文子串
                if (s.charAt(i) == s.charAt(i+k)) {
                    if (k < 2 || dp[i+1][i+k-1]) {
                        dp[i][i+k] = true;
                        res ++;
                    }
                }
            }
        }
        return res;
    }

方法二,以当前元素为中轴元素,向两边扩散,计算个数。注意中轴元素不是只是单个字符,2个字符也可以作为中轴元素(如abba,子串bb无法通过任何一个单个字符扩散得到)。而3个字符可以由一个字符扩散得到。

public int countSubstrings(String s) {
        int len = s.length();
        int res = 0;
        for (int i = 0; i < len; i++) {
            res += countNum(s, i, i); //以i为轴心的回文子串个数
            res += countNum(s, i, i+1); //以i,i+1为轴心的回文子串个数
        }
        return res;
    }
 
    public int countNum(String s, int left, int right) {
        int count = 0;
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            left --; //往两边扩散
            right ++;
            count ++;
        }
        return count;
    }

例题409. 最长回文串。给定一个包含大写字母和小写字母的字符串 s ,返回 通过这些字母构造成的 最长的回文串 。在构造过程中,请注意 区分大小写 。比如 “Aa” 不能当做一个回文字符串。

  • 再看一道回文串的题,但这道题没什么特殊解法,直接统计字符出现次数即可;
  • 偶数长度每个字符必须出现偶数次,奇数长度允许一个字符出现一次;
  • 注意这道题可能出现大小写字母,因此初始化数组长度为 58,若有其他 ASIIC 字符,可初始化为 128.
    public int longestPalindrome(String s) {
        char[] sarr = s.toCharArray();
        int[] dic = new int[58]; //可能出现大小写字符
        int res = 0;
        for (char c : sarr) {
            dic[c - 'A'] ++;
        }
        for (int d : dic) {
            res += d % 2 == 0 ? d : d - 1;
        }
        return res < sarr.length ? res + 1 : res;
    }

例题 696. 计数二进制子串。给定一个字符串 s,统计并返回具有相同数量 0 和 1 的非空(连续)子字符串的数量,并且这些子字符串中的所有 0 和所有 1 都是成组连续的。重复出现(不同位置)的子串也要统计它们出现的次数。

  • 本来想通过一个数进行统计,当出现 0 加一 ,出现 1 减一,等于0时则说明出现满足要求的子串,但是这样没法保证0和1是连续的;
  • 因此可以通过两个数进行统计,pre 统计前一种数出现的次数,cur 统计当前数出现的次数,当 pre >= cur,则表明出现满足要求的子串。
public int countBinarySubstrings(String s) {
        int count = 0;
        int pre = 0;
        int cur = 1;
        char[] sarr = s.toCharArray();
        for (int i = 1; i < sarr.length; i++) {
            if (sarr[i] == sarr[i-1]) {
                cur ++;
            } else {
                //当数字发生变化就重新统计,满足了连续性
                pre = cur;
                cur = 1;
            }
            //当统计的前一种数的数量大于等于现在这一种,则表明可以构成一个满足要求的子串
            if (pre >= cur) {
                count ++;
            }
        }
        return count;
    }
  • 还有一种解法,统计连续的 0 和连续的 1 出现的次数,再取相邻个数的最小值相加即是结果。( 0011011,统计次数为2212,相邻个数最小值相加为 2+1+1 = 4)

2. 字符串理解

例题 227. 基本计算器 II。给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。整数除法仅保留整数部分。你可以假设给定的表达式总是有效的。所有中间结果将在 [-231, 231 - 1] 的范围内。字符串内可能有空格。

  • 思路是用一个栈存储数据,运算符用一个字符存储;减法转化为加法;乘除由于优先级比较高直接计算。最后存储到栈中的数据都只是需要做加法的数据。
  • 有几个坑,首先数字可能有多位,需要合并成一个数;其次要注意空格的处理,一般空格可以跳过,但是最后一个字符如果是空格需要处理,否则会少计算一位数。
public int calculate(String s) {
        Stack<Integer> stack = new Stack<>();
        char[] sarr = s.toCharArray();
        int p = 0;
        char sign = '+';
        for (int i = 0; i < sarr.length; i++) {
        	//如果一直是数字就需要先一直合并
            if (sarr[i] >= '0' && sarr[i] <= '9') {
                p = p * 10 + (sarr[i] - '0'); //可能有多位数
            }
            //注意这里如果到了最后一个字符都需要进入处理,本来空格不需要处理,但是如果最后一个字符是空格也需要处理
            if (((sarr[i] < '0' || sarr[i] > '9') && sarr[i] != ' ') || i == sarr.length-1) {
                //当前字符为符号,sign存储的是前一个符号
                //将当前符号之前的数存储进栈
                if (sign == '+') {
                    stack.push(p);
                } else if (sign == '-') {
                    stack.push(-p);
                } else {
                    //乘除优先级更高直接计算
                    int tmp = sign == '*' ? stack.peek() * p : stack.peek() / p;
                    stack.pop();
                    stack.push(tmp);
                }
                sign = sarr[i];
                p = 0;
            }
        }
        int res = 0;
        //上述遍历完之后,栈中只存在需要相加的数
        while (!stack.isEmpty()) {
            res += stack.pop();
        }
        return res;
    }

3. 字符串匹配

例题 28,实现 strStr()。给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。

  • 直接朴素搜索即可。
  • 此题还有所谓的 KMP 算法,时间复杂度 O(m+n),先mark一下吧。
public int strStr(String haystack, String needle) {
        if ("".equals(needle)) {
            return 0;
        }
        char[] harr = haystack.toCharArray();
        char[] narr = needle.toCharArray();
        int h = 0;
        int n = 0;
        while(h < harr.length) {
            if (harr[h] == narr[n]) {
                int tmp = h;
                while (tmp < harr.length && n < narr.length && harr[tmp] == narr[n]) {
                    tmp++;
                    n++;
                }
                if (n >= narr.length) {
                    return h;
                } 
                n = 0;
            }
            h++;
        }
        return -1;
    }

二. 链表

1. 基础操作

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

  • 非递归写法,三个指针分别指向当前节点,下一节点,上一节点,循环处理直到链尾。
public ListNode reverseList(ListNode head) {
        ListNode cur = head;
        ListNode next = null;
        ListNode pre = null;
        while(cur != null) {
            next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }
  • 递归写法。与非递归大同小异,循环改用递归实现,都是要先记录下当前节点的 next 节点,避免丢失链表。
  • 尾递归直接返回了头节点。
public ListNode reverseList(ListNode head) {
        return reverseNode(head, null);
    }

    public ListNode reverseNode(ListNode cur, ListNode pre) {
        if (cur == null) {
            return pre;
        }
        ListNode next = cur.next;
        cur.next = pre;
        return reverseNode(next, cur);
    }

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

public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode node1 = list1;
        ListNode node2 = list2;
        //新链表的头节点,先让它指向某个节点
        ListNode head = new ListNode(0);
        ListNode node = head;
        while(node1 != null && node2 != null) {
            if (node1.val < node2.val) {
                node.next = node1;
                node1 = node1.next;
            } else {
                node.next = node2;
                node2 = node2.next;
            }
            node = node.next;
        }
        node.next = node1 != null ? node1 : node2;
        return head.next;
    }

例题 24. 两两交换链表中的节点。给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

  • 画图分析一下就出来了,注意考虑奇数节点数和偶数节点数的区别。
public ListNode swapPairs(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode node = head;
        //记录head.next,是返回链表的头节点
        ListNode next1 = head.next;
        ListNode next2 = null;
        while (node != null && node.next != null) {
            next2 = node.next.next;
            node.next.next = node;
            //注意这里需要分情况考虑node.next是啥
            node.next = (next2 == null || next2.next == null) ? next2 : next2.next;
            node = next2;
        }
        return next1;
    }
  • 优雅的递归写法。
public ListNode swapPairs(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode next1 = head.next;
        ListNode next2 = head.next.next;
        head.next.next = head;
        head.next = swapPairs(next2);
        return next1;
    }

2. 其他链表技巧

例题 160. 相交链表。给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

  • 两个链表同时走,链表A走完一次后将节点置为 headB,同样链表B走完一次后将节点置为 headA,此时再继续走的话两个链表第一次相遇的节点就是相交节点;
  • 无需事先判断是否相交,因为如果不相交,while循环退出的条件是两个节点都为 null,也满足要求。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        //找相交节点
        ListNode nodeA = headA;
        ListNode nodeB = headB;
        while (nodeA != nodeB) {
            nodeA = nodeA == null ? headB : nodeA.next;
            nodeB = nodeB == null ? headA : nodeB.next;
        } 
        return nodeA;
    }

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

  • 要用O(1)复杂度解决,先用快慢指针找中点,再翻转后半部分链表进行比较。
public boolean isPalindrome(ListNode head) {
		//快慢指针找中点
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        fast = head;
        //翻转链表
        ListNode next = null;
        ListNode pre = null;
        while (slow != null) {
            next = slow.next;
            slow.next = pre;
            pre = slow;
            slow = next;
        }
        //开始比较
        while (pre != null) {
            if (pre.val != fast.val) {
                return false;
            }
            pre = pre.next;
            fast = fast.next;
        }
        return true;
    }

例题 19,删除链表的倒数第 N 个结点。给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

  • 重点在于如何不遍历2次就找到倒数第 N 个节点;
  • 同样使用快慢指针,快指针先走n步,然后一起走,直到快指针走到链尾 ,此时慢指针就走到了要删除节点的前一个节点;
  • 要注意可能删除头节点,因此初始化一个新的头节点指向head。
public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode myHead = new ListNode(0);
        myHead.next = head;
        ListNode fast = myHead;
        ListNode slow = myHead;
        while (n > 0) { 
            fast = fast.next;
            n--;
        }
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
        //此时slow走到要删除节点的前一个节点
        ListNode tmp = slow.next;
        slow.next = tmp.next;
        tmp.next = null;
        return myHead.next;
    }

例题 148. 排序链表。给你链表的头结点 head ,请将其按升序排列并返回排序后的链表 。

  • 快慢指针找中点将链表断开,然后归并排序。
class Solution {
    public ListNode sortList(ListNode head) {
        return divideList(head);
    }

    //返回的是排序后的链表(先拆分至最小再合并)
    public ListNode divideList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode fast = head.next; //fast先走一步
        ListNode slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        ListNode head2 = slow.next;
        slow.next = null;
        ListNode l = divideList(head);
        ListNode r = divideList(head2);
        return mergeTowList(l, r);
    }

    //将两个排序的链表合并成一条排序链表
    public ListNode mergeTowList(ListNode head1, ListNode head2) {
        ListNode node1 = head1;
        ListNode node2 = head2;
        ListNode head = new ListNode(0);
        ListNode node = head;
        while (node1 != null && node2 != null) {
            if (node1.val < node2.val) {
                node.next = node1;
                node1 = node1.next;
            } else {
                node.next = node2;
                node2 = node2.next;
            }
            node = node.next;
        } 
        node.next = node1 == null ? node2 : node1;
        return head.next;
    }
}
为了找到给定字符串 `t` 中相对于字符串 `s` 新添加的字母,你可以通过比较两者的字符集和长度来实现。以下是使用 C 语言的一个简单步骤: 1. 首先,计算字符串 `s` 的字符集(所有独特字符)。 2. 然后,计算字符串 `t` 的字符集。 3. 比较这两个集合,找出 `t` 中 `s` 所不具备的字符。 下面是一个简单的 C 代码示例: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #define ALPHABET_SIZE 26 int isPresent(char c, char* chars) { for (int i = 0; i < strlen(chars); i++) { if (chars[i] == c) return 1; } return 0; } char findNewCharacter(const char* s, const char* t) { char sChars[ALPHABET_SIZE] = {0}; // 记录s中的字符 for (char c : s) { sChars[c - 'a'] = 1; } char tChars[ALPHABET_SIZE]; // 初始化t字符计数 for (int i = 0; t[i]; i++) tChars[t[i] - 'a']++; // 查找新添加的字符 for (int i = 0; i < ALPHABET_SIZE; i++) { if (sChars[i] != tChars[i]) { return 'a' + i; } } // 如果不存在新字符,返回一个假设字符 return 'z' + 1; } int main() { char s[] = "example"; char t[] = "pxamleb"; // 假设t是由s随机重排并添加了'e' char newChar = findNewCharacter(s, t); printf("The new character added in string t is '%c'.\n", newChar); return 0; } ``` 在这个代码里,`isPresent` 函数检查字符是否在 `sChars` 中,`findNewCharacter` 函数找出新增加的字母。当你运行这个程序,它会输出 `'e'`,因为 `'e'` 是 `t` 中 `s` 不包含的字符。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值