上一章我们详细的介绍了二分搜索树的底层实现。这章我们介绍两个高层的数据结构,集合和映射。这种高层的数据结构,更像是我们定义好了使用接口规则,但是具体的底层实现可以是多种多样的。
集合
承载元素的容器,元素的去重操作。回忆我们上一小节实现的二分搜索树是不能盛放重复元素的,非常好的实现“集合”的底层数据结构。
Set<E> void add(E) void remove(E) boolean contains(E) int getSize() boolean isEmpty()
添加元素,删除元素,是否包含,大小,是否为空。连续add两次,只保留一份。典型应用: 客户统计。典型应用:词汇量统计。
BST依然是上一章实现的BST
package cn.mtianyan;public class BSTSet<E extends Comparable<E>> implements Set<E> { private BST<E> bst; public BSTSet() { bst = new BST<>(); } @Override public void add(E e) { bst.add(e); // 本身就可以对于重复不理会 } @Override public void remove(E e) { bst.remove(e); } @Override public boolean contanins(E e) { return bst.contains(e); } @Override public int getSize() { return bst.getSize(); } @Override public boolean isEmpty() { return bst.isEmpty(); } }
package cn.mtianyan;import java.io.FileInputStream;import java.util.ArrayList;import java.util.Scanner;import java.util.Locale;import java.io.File;import java.io.BufferedInputStream;import java.io.IOException;/** * 文件相关操作类 */public class FileOperation { /** * 读取文件名称为filename中的内容,并将其中包含的所有词语放进ArrayList words中 * * @param filename * @param words * @return */ public static boolean readFile(String filename, ArrayList<String> words){ if (filename == null || words == null){ System.out.println("filename is null or words is null"); return false; } // 文件读取 Scanner scanner; try { File file = new File(filename); if(file.exists()){ FileInputStream fis = new FileInputStream(file); scanner = new Scanner(new BufferedInputStream(fis), "UTF-8"); scanner.useLocale(Locale.ENGLISH); } else return false; } catch(IOException ioe){ System.out.println("Cannot open " + filename); return false; } // 简单分词 // 这个分词方式相对简陋, 没有考虑很多文本处理中的特殊问题 // 在这里只做demo展示用 if (scanner.hasNextLine()) { String contents = scanner.useDelimiter("\\A").next(); int start = firstCharacterIndex(contents, 0); for (int i = start + 1; i <= contents.length(); ) if (i == contents.length() || !Character.isLetter(contents.charAt(i))) { String word = contents.substring(start, i).toLowerCase(); words.add(word); start = firstCharacterIndex(contents, i); i = start + 1; } else i++; } return true; } /** * 寻找字符串s中,从start的位置开始的第一个字母字符的位置 * * @param s * @param start * @return */ private static int firstCharacterIndex(String s, int start){ for( int i = start ; i < s.length() ; i ++ ) if( Character.isLetter(s.charAt(i)) ) return i; return s.length(); } }
这里是一个文件读取单词后存入一个ArrayList words中的操作。
传入文件名,将每一个单词扔进数组中。使用英文文本,简单的分词,一行一行读取,把每一个词分出来,文本中所有的单词。
NLP中对于一个动词的不同形式等可以分为一个单词,我们这里只是最简单的就是字母不同的单词。
傲慢与偏见,双城记。年代久远,版权可以使用。
package cn.mtianyan;import java.util.ArrayList;public class Main { public static void main(String[] args) { System.out.println("Pride and Prejudice"); ArrayList<String> words1 = new ArrayList<>(); if (FileOperation.readFile("pride-and-prejudice.txt", words1)) { System.out.println("Total words: " + words1.size()); BSTSet<String> set1 = new BSTSet<>(); for (String word : words1) set1.add(word); System.out.println("Total different words: " + set1.getSize()); } System.out.println(); System.out.println("A Tale of Two Cities"); ArrayList<String> words2 = new ArrayList<>(); if (FileOperation.readFile("a-tale-of-two-cities.txt", words2)) { System.out.println("Total words: " + words2.size()); BSTSet<String> set2 = new BSTSet<>(); for (String word : words2) set2.add(word); System.out.println("Total different words: " + set2.getSize()); } } }
傲慢与偏见12万词,去重后只有六千多词。读原版书,读感兴趣的书是学英语的好方式。
双城记 14万词,去重后有九千多次。单词的多少是阅读这本书难度指标的一项。
基于链表的集合实现
为什么我们要再进行一下基于链表的实现呢?因为二分搜索树和LinkedList都属于动态数据结构,它们的数据都存储在一个一个的node中。
class Node{ E e; Node left; Node right; }
二分搜索树中指向左右子树。
class Node{ E e; Node next ; }
链表中指向下一个节点。两种不同实现之后我们可以比较性能。
public class LinkedListSet<E> implements Set<E>{ private LinkedList<E> list; public LinkedListSet() { list = new LinkedList<>(); } }
基于链表的实现中,并不要求传入的类型具有可比性,这是线性数据结构的特点。
package cn.mtianyan.set;import cn.mtianyan.linked.LinkedList;public class LinkedListSet<E> implements Set<E>{ private LinkedList<E> list; public LinkedListSet() { list = new LinkedList<>(); } @Override public boolean contanins(E e) { return list.contains(e); } @Override public int getSize() { return list.getSize(); } @Override public boolean isEmpty() { return list.isEmpty(); } @Override public void add(E e) { // 保证不能重复e if (!list.contains(e)) list.addFirst(e); } @Override public void remove(E e) { list.removeElement(e); } }
public static void main(String[] args) { System.out.println("Pride and Prejudice"); ArrayList<String> words1 = new ArrayList<>(); if (FileOperation.readFile("pride-and-prejudice.txt", words1)) { System.out.println("Total words: " + words1.size()); LinkedListSet<String> set1 = new LinkedListSet<>(); for (String word : words1) set1.add(word); System.out.println("Total different words: " + set1.getSize()); } System.out.println(); System.out.println("A Tale of Two Cities"); ArrayList<String> words2 = new ArrayList<>(); if (FileOperation.readFile("a-tale-of-two-cities.txt", words2)) { System.out.println("Total words: " + words2.size()); LinkedListSet<String> set2 = new LinkedListSet<>(); for (String word : words2) set2.add(word); System.out.println("Total different words: " + set2.getSize()); } }
运行结果一模一样,但是明显感觉到时间变长。
集合类的复杂度分析
前面我们已经实现了两种底层数据结构不同的Set实现。一个是基于二分搜索树的一个是基于链表的。
之前的测试中,基于链表的是明显的要慢的。
package cn.mtianyan;import cn.mtianyan.set.BSTSet;import cn.mtianyan.set.LinkedListSet;import cn.mtianyan.set.Set;import java.util.ArrayList;public class CompareTwoBstLinkedListSet { private static double testSet(Set<String> set, String filename){ long startTime = System.nanoTime(); System.out.println(filename); ArrayList<String> words = new ArrayList<>(); if(FileOperation.readFile(filename, words)) { System.out.println("Total words: " + words.size()); for (String word : words) set.add(word); System.out.println("Total different words: " + set.getSize()); } long endTime = System.nanoTime(); return (endTime - startTime) / 1e9; } public static void main(String[] args) { String filename = "pride-and-prejudice.txt"; BSTSet<String> bstSet = new BSTSet<>(); double time1 = testSet(bstSet, filename); System.out.println("BST Set: " + time1 + " s"); System.out.println(); LinkedListSet<String> linkedListSet = new LinkedListSet<>(); double time2 = testSet(linkedListSet, filename); System.out.println("Linked List Set: " + time2 + " s"); } }
运行结果:
可以看到二分搜索树是要性能优于链表的。
集合的时间复杂度分析
集合不涉及改。
本来链表添加只需要O(1)时间复杂度,但是我们为了保证不重复,先调用了一遍contains,因此变成了O(n)
contains操作,我们必须要从头到尾扫一遍链表,复杂度O(n)
remove操作,要先找到待删除元素前一个元素,时间复杂度O(n)
如果有n个单词,每个单词都不同,是o(n^2)级别的。
添加一个元素,走左子树,就不会去右子树,节省了很大一部分的寻找开销,最多能经历的节点数是树的高度。添加元素,删除元素,查找元素都是这样的,对于它来说,时间复杂度为O(h),h为二分搜索树的高度。
下面我们来谈一谈高度n和h之间的关系,极端: 满二叉树中,第h-1层,有2^(h-1)个节点。
h层一共有多少个节点?
根据等比数列的求和公式,可以得到h层一共有2^h-1 = n
通常我们不会计较这里的底,无论是以2位底,还是10,它们都是logN级别的。
logn和n的差距
相差了五万倍,一秒跑完,14个小时。一天跑完,137年跑完。
logn是一个非常快的时间复杂度,很多排序算法是nlogn的,比n^2快了很多很多(前面都乘以n)。但是这里我们的logn必须要注明是平均的,因为是在满二叉树下计算出来的。
同样的数据,可以对应不同的二分搜索树
1,2,3,4,5,6
它也可以对应一个1到6排列的类似链表的,从根节点到下面每个节点都只有右子树的一个二分搜索树。
对于这种等同于链表的二分搜索树,它的高度等于节点的个数,它将退化成与链表一模一样的一棵树。这种最坏的顺序只要我们是按照123456的顺序创建就可以形成。
二分搜索树会退化为链表,虽然平均来讲是O(logn)级别的,但是当退化成链表的最差情况,会变成O(n)级别的。
解决这个问题的方法就是要来创建平衡二叉树,在课程比较靠后的位置会给大家讲解。最准确的二分搜索树的时间复杂度是O(h)
LeetCode中的集合相关问题以及更多集合相关问题。
804号问题:
https://leetcode-cn.com/problems/unique-morse-code-words/description/
将单词扔进集合中,然后看集合自动去重之后,剩下的集合大小。我们使用java的TreeSet进行实现,它的底层是一颗平衡二叉树(红黑树实现,不会出现最差情况,定义了更多操作)。
package cn.mtianyan.leetcode_804;import java.util.TreeSet;class Solution { public int uniqueMorseRepresentations(String[] words) { String[] codes = {".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.","---",".--.","--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."}; TreeSet set = new TreeSet(); for (String word : words) { StringBuffer res = new StringBuffer(); for (int i = 0; i < word.length(); i++) { res.append(codes[word.charAt(i) - 'a']); //a充当一个初始的偏移,a-a=0 b-a=1 } set.add(res.toString()); } return set.size(); } }
运行结果:
有序集合和无序集合
我们之前的基于二分搜索树实现的集合,java中红黑树实现的集合本质都是有序的集合。
有序集合中的元素具有顺序性;可以从小到大遍历,可以查看前驱后继。
而链表实现的集合,实际上是一个无序的集合。我们只是根据元素插入的顺序决定了它在集合中的顺序,不能轻易的进行从小到大等操作。
就像我们刚做的那个LeetCode的题,就用不到集合的有序性,因此我们可以采用无序集合。基于哈希表的实现比搜索树更快。
搜索树保持了有序性,此时它付出的代价就是时间复杂度上稍微差于哈希表。
多重集合
集合容纳重复元素,集合中的元素可以重复,在允许重复元素的二分搜索树基础上实现。
映射Map
函数一一映射。
作者:天涯明月笙
链接:https://www.jianshu.com/p/52e224271f93