题目35:在字符串中找出第一个只出现一次的字符。如输入“abaccdeff”,则输出‘b’。
分析:
最直观的想法是从头开始扫描这个字符串中的每个字符。当访问到某字符时拿这个字符和后面的每个字符相比较,如果在后面没有发现重复的字符,则该字符就是只出现一次的字符。如果字符串有n个字符,每个字符可能与后面的O(n)个字符相比较,因此这种思路的时间复杂度是O(n2),并不理想。
可以定义一个哈希表(外部空间),其键值(Key)是字符,而值(Value)是该字符出现的次数。
同时我们还需要从头开始扫描字符串两次:
(1)第一次扫描字符串时,每扫描到一个字符就在哈希表的对应项中把次数加1。(时间效率O(n))
(2)第二次扫描时,每扫描到一个字符就能从哈希表中得到该字符出现的次数。这样第一个只出现一次的字符就是符合要求的输出。(时间效率O(n))
这样算起来,总的时间复杂度仍然是O(n)。
字符(char)是一个长度为8的数据类型,因此总共有256种可能。我们创建一个长度为256的数组来模拟哈希表,每个字母根据其ASCII码值作为数组的下标对应数组的一个数字,而数组中存储的是每个字符出现的次数。计算下来,它的大小是256*4字节(1个int类型在Windows下占4个字节)=1K。由于这个数组的大小是个常数,因此可以认为这种算法的空间复杂度是O(1)。
代码如下所示:
public static char firstNotRepeatingChar(String str) {
if (str == null || str.length() == 0) return '\0';
char[] array = str.toCharArray();
int size = 256;
// 借助数组来模拟哈希表,只用1K的空间消耗
int[] hastTable = new int[size];
// 初始化数组
for (int i = 0; i < size; i++) {
hastTable[i] = 0;
}
for (int i = 0; i < array.length; i++) {
hastTable[array[i]]++;
}
for (int i = 0; i < array.length; i++) {
if (hastTable[array[i]] == 1) return array[i];
}
return '\0';
}
public static void main(String[] args) {
System.out.println(firstNotRepeatingChar("abaccdeff"));
System.out.println(firstNotRepeatingChar("qwerqwer"));
System.out.println(firstNotRepeatingChar("qwertyuiop"));
System.out.println(firstNotRepeatingChar(""));
}
变型 1:若题目考虑输入汉字呢?
变型 2:定义一个函数,输入两个字符串,从第一个字符串中删除在第二个字符串中出现过的所有字符。例如:从第一个字符串“We are student。”中删除在第二个字符串“aeiou”中出现的字符,得到的结果是“W r stdnts”。
分析:
可以创建一个用数组实现的简单哈希表来存储第二个字符串。这样我们从头到尾扫描第一个字符串的每一个字符时,用O(1)时间就能判断出该字符是不是在第二个字符中。如果第一个字符串的长度是n,那么总的时间复杂度是O(n)。
代码如下所示:
public static String firstNotRepeatingChar(String str1, String str2) {
if (str1 == null || str1.length() == 0 ) return "";
if (str2 == null || str2.length() == 0) return str1;
char[] array1 = str1.toCharArray();
char[] array2 = str2.toCharArray();
StringBuilder sb = new StringBuilder();
int size = 256;
// 借助数组来模拟哈希表,只用1K的空间消耗
int[] hastTable = new int[size];
//初始化哈希表
for (int i = 0; i < hastTable.length; i++)
hastTable[i] = 0;
for (int i = 0; i < array2.length; i++) {
hastTable[array2[i]] = 1;
}
for (int i = 0; i < array1.length; i++) {
if( hastTable[array1[i]] != 1) sb.append(array1[i]);
}
return sb.toString();
}
public static void main(String[] args) {
System.out.println(firstNotRepeatingChar("We are students","aeiou"));
System.out.println(firstNotRepeatingChar("We are students",""));
System.out.println(firstNotRepeatingChar("","aeiou"));
}
变型 3:定义一个函数,删除字符串中所有重复出现的字符。例如输入“google”,删除重复的字符之后的结果是"gole"。
分析:
可以创建一个用布尔类型数组实现的简单的哈希表。数组中的元素的意义是其下标看做ASCII码后对应的字母在字符串中是否已经出现。先把数组中的元素都设为false。以“google”为例,当扫描到第一个g时,g的ASCII码是103,那么把数组中下标为103的元素的值设为true。当扫描到第二个g时,发现数组中下标为103的元素的值是true,就知道g在前面已经出现了。也就是说,用O(1)时间就能判断出每个字符是否在前面出现过。如果字符串长度是n,那么总的时间复杂度是O(n)。
变型 4:在英语中,如果两个单词中出现的字符相同,并且每个字母出现的次数也相同,那么这两个单词互为变位词。例如silent与listen、evil与live等互为变位词。请完成一个函数,判断输入的两个字符串是不是互为变位词。
分析:
可以创建一个用数组实现的简单哈希表,用来统计字符串中每个字符出现的次数。当扫描到第一个字符串中的每个字符时,为哈希表对应的项的值增加1;接下来扫描第二个字符串,扫描到每个字符时,为哈希表对应的项的值减去1。如果扫描完第二个字符串后,哈希表中所有的值都是0,那么这两个字符串就互为变位词。
小结:如果需要判断多个字符是不是在某个字符串里出现过或者统计多个字符在某个字符串中出现的次数,那么我们可以考虑基于数组创建一个简单的哈希表,这样可以用很小的空间消耗换来时间效率的提升。
题目37:输入两个链表,找出它们的第一个公共结点。链表结点定义如下。
public class Node<T> {
public T item;
public Node<T> next;
public Node() {
}
public Node(T item) {
this.item = item;
}
}
分析:
最直观的的想法是:在第一链表上顺序遍历每个结点,每遍历到一个结点的时候,在第二个链表上顺序遍历每个结点。如果在第二个链表上有一个结点和第一个链表上的结点一样,说明两个链表在这个结点上重合,于是就找到了它们的公共结点。如果第一个链表的长度为m,第二个链表的长度为n,显然该方法的时间复杂度是O(mn)。
首先,经过分析发现两个有公共结点而部分重合的链表,拓扑形状看起来像一个Y,而不可能像X,如下图所示,两个链表在值为6的结点处交汇:
如果两个链表有公共结点,那么公共结点出现在两个链表的尾部。如果从两个链表的尾部开始往前比较,最后一个相同的结点就是要找的结点。但是,在单链表中只能从头结点开始按顺序遍历,最后才能到达尾结点。最后到达的尾结点却要最先被比较,这是“后进先出”的特性。于是,可以使用栈的特点来解决这个问题:分别把两个链表的结点放入两个栈里,这样两个链表的尾结点就位于两个栈的栈顶,接下来比较两个栈顶的结点是否相同。如果相同,则把栈顶弹出接着比较下一个栈顶,直到找到最后一个相同的结点。这种方法需要用两个辅助栈。如果链表的长度分别为m和n,那么空间复杂度是O(m+n),复杂度也是O(m+n)。和上一种方法相比,时间效率得到了提高,相当于是用空间消耗换取了时间效率。
代码如下所示:
public static Node findFirstCommonNode(Node pNode1, Node pNode2) {
Node result = null;
if (pNode1 == null || pNode2 == null) return result;
Stack<Node> stack1 = new Stack<>();
Stack<Node> stack2 = new Stack<>();
Node head1 = pNode1;
Node head2 = pNode2;
while (head1 != null) {
stack1.push(head1);
head1 = head1.next;
}
while (head2 != null) {
stack2.push(head2);
head2 = head2.next;
}
while (stack1.size() != 0 && stack2.size() != 0 && stack1.peek() == stack2.peek()) {
result = stack1.pop();
stack2.pop();
}
return result;
}
public static void main(String[] args) {
// 1-2-3-6-7
// 4-5-6-7
Node node1 = new Node(1);
Node node2 = new Node(2);
Node node3 = new Node(3);
Node node4 = new Node(4);
Node node5 = new Node(5);
Node node6 = new Node(6);
Node node7 = new Node(7);
// first
// node1.next = node2;
// node2.next = node3;
// node3.next = node6;
// node6.next = node7;
// second
// node4.next = node5;
// node5.next = node6;
// System.out.println(findFirstCommonNode(node1,node4).item);
// 没有公共结点
// 1-2-3-4
// 5-6-7
// first
// node1.next = node2;
// node2.next = node3;
// node3.next = node4;
// second
// node5.next = node6;
// node6.next = node7;
// System.out.println(findFirstCommonNode(node1,node5));
// 最后一个结点是公共结点
// 5-6-7
// 1-2-3-4-7
// first
// node1.next = node2;
// node2.next = node3;
// node3.next = node4;
// node4.next = node7;
// second
// node5.next = node6;
// node6.next = node7;
// System.out.println(findFirstCommonNode(node1,node5).item);
// 公共结点是第一个结点
// 1 - 2 - 3 - 4 - 5
// first & second
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
System.out.println(findFirstCommonNode(node1, node1).item);
// 空链表
System.out.println(findFirstCommonNode(node1, null));
System.out.println(findFirstCommonNode(null, null));
}
还有一种不借助外部空间的方法,首先遍历两个链表得到它们的长度,就能知道哪个链表比较长,以及长的链表比短的链表多几个结点。在第二次遍历的时候,在较长的链表上先走若干步,接着再同时在两个链表上遍历,找到的第一个相同的结点就是它们的第一个公共结点。比如在上图的两个链表中,可以先遍历一次得到它们的长度分别为5和4,也就是较长的链表与较短的链表相比多一个结点。第二次先在长的链表上走1步,到达结点2。接下来分别从结点2和结点4出发同时遍历两个结点,直到找到它们第一个相同的结点6。这种方法的时间复杂度也是O(m+n),但我们不再需要辅助的栈,因此提高了空间效率。
代码如下所示:
public static Node findFirstCommonNode(Node pNode1, Node pNode2) {
int num1 = getLength(pNode1);
int num2 = getLength(pNode2);
int dif = num1 - num2;
Node nodeLong = pNode1;
Node nodeShort = pNode2;
if (dif < 0) {
dif = num2 - num1;
nodeLong = pNode2;
nodeShort = pNode1;
}
for (int i = 0; i < dif; i++) {
nodeLong = nodeLong.next;
}
while (nodeLong != null && nodeShort != null & nodeLong != nodeShort) {
nodeLong = nodeLong.next;
nodeShort = nodeShort.next;
}
return nodeLong;
}