一. 字符串
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;
}
}
1900

被折叠的 条评论
为什么被折叠?



