背景:接下来要学习集合Set,这种高层的数据结构。所谓“高层的数据结构”,就是我们先定义好该数据结构的使用接口,有了这些使用接口和该数据结构本身维持的性质(或本身具有的特点),我们就可以在应用中使用它们,而它们的具体实现可以是是多种多样的。类似的数据结构如栈、队列。例如,栈定义了入栈和出栈等使用接口或操作;特点:后进先出;可以基于数组和链表进行实现。它可以解决“后进先出”类型的问题,如:系统栈程序调用、子弹射击等
一 集合
特点:每一个元素只存在一份,即不存储重复元素应用:依据该数据结构的特点——“去重”,主要是客户统计、词汇量统计等。
二 接口及实现
集合的接口定义如下:
public interface Set<E> {
void add(E e);
void remove(E e);
boolean contains(E e);
int getSize();
boolean isEmpty();
}
实现可以基于二分搜索树或链表等。
高层的数据结构,是可以有多种实现的,我们常常会比较各种不同实现之间的性能。使用的手段即是时间复杂度分析
基于二分搜索树和单链表的实现的时间复杂度如下图所示:
在满的二分搜索树的情况下,可以推导出高度h和结点数n的关系为:2的h方 - 1 = n, 即 h = log以2为底 n - 1。在O的定义下,可以忽略底数。
最准确的时间复杂度是O(h), 平均时间复杂度是O(logn),最差时间复杂度,即二分搜索树退化成链表时(二分搜索树的局限性),O(n)
三 词汇量统计
1 文本的简单分词
在统计,一篇文章或一本著作的词汇量的时候,首先要将文件分解为单个的单词。下面介绍一种简单的分词方法:import java.io.*;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Scanner;
public class FileOperation {
/**
* 读取filename文件中的单词并放入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));
// 设置文本扫描器要处理的语言
scanner.useLocale(Locale.ENGLISH);
}else
return false;
} catch (IOException ioe) {
System.out.println("Cannot open " + filename);
return false;
}
// 简单分词代码主要部分
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();
}
}
下面对分词代码的主要部分和firstCharacterIndex方法等进行注释。
首先,说明下分词逻辑,如下图所示:
首先,start变量定位文本中的第一个字母,即单词的首字母。然后从start + 1位置,使用字母指针i逐个向后扫描,直到出现非字母(空格)或扫描到文本尾部停止扫描,索引区域[start, end)即是一个完整的单词。
int firstCharacterIndex(String s, int start)
方法,显然是用来查找文本中的单词首字母的。scanner.hasNextLine()
只要文本结束符号EoF前存在内容,就会返回true。加入文本中存在三行内容,while(scanner.hasNextLine()),前三次返回true,第四次判断时,scanner.hasNextLine()应该是遇到了文件结束符EoF,因此返回false。
scanner.useDelimiter("\\A").next()
,它由两部分组成,第一部分scanner.useDelimiter("\\A")
,其中“\A”是java中的正则表达式,它设置了文本扫描器的分隔符为文本的开头,默认情况下是空格</ font>
如上图所示,以空格为分隔符,由于文本中有两个空格,因此被“切割了两刀”,文本被分成了三个部分,每个部分称为一个token。同理,由于文本只能有一个“文本的开始”,因此被“切割了一刀”,文本只被分成了一个部分,即一个token
scanner.next()
,按顺序读取文本中的token,由于整个文本被分成了一个token,因此,此代码读取了全部文本。
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 ++;
}
这段代码的功能上面已经描述过了:首先,start变量定位文本中的第一个字母,即单词的首字母。然后从start + 1位置,使用字母指针i逐个向后扫描,直到出现非字母(空格)或扫描到文本尾部停止扫描,索引区域[start, end)即是一个完整的单词
contents.substring(start, i)
String类一个取子串的方法,对于区间是左闭右开的,即start索引对应的字符可以取到,i索引对应的字符取不到
2 进行词汇量统计
import java.util.ArrayList;
import java.util.Set;
import java.util.TreeSet;
public class Main {
public static void main(String[] args) {
System.out.println("《Pride and Prejudice》");
ArrayList<String> words = new ArrayList<>();
FileOperation.readFile("pride-and-prejudice.txt", words);
System.out.println("Total words: " + words.size());
Set<String> diffWords = new TreeSet<>();
for(String word: words)
diffWords.add(word);
System.out.println("Total different words: " + diffWords.size());
}
}
注意:使用上述的代码实现,pride-and-prejudice.txt文件需要放在工作目录下。System.getProperty("user.dir")
可以查看当前的工作目录。txt文本可以在网上下载。
执行结果如下:
四 有序集合和无序集合
1 有序集合中的元素具有顺序性,基于搜索树的实现
例如,基于二分搜索树、java标准库TreeSet的基于红黑树实现的集合,其有序性体现在:可以从小到大遍历集合中的所有元素;可以很容易找到集合中最小元素、最大元素;查找某个元素的上一个或下一个元素等
1 无序集合中的元素没有顺序性,基于哈希表的实现