关注公众号:EZ大数据(ID:EZ_DATA)。每天进步一点点,感觉很爽!
昨天说完二叉搜索树,今天来个简单点的:集合Set和映射Map。然后我们再使用链表和二叉树来初步实现Set和Map的功能,并研究下相关操作的时间复杂度。
OK,正餐开始(我是来对比树和链表的,别搞错)!
说起集合Set,其里面的元素不会重复,所以,经常使用集合来做去重操作。而日常工作中,典型的应用有:客户统计、文本词汇量统计等。
首先我们使用之前的链表来实现集合Set。
void add(E)
void remove(E)
boolean contains(E)
int getSize()
boolean isEmpty()
话不多少,直接上代码:
import java.util.ArrayList;
public class LinkedListSet<E> implements Set<E> {
private LinkedList<E> list;
public LinkedListSet(){
list = new LinkedList<>();
}
@Override
public int getSize(){
return list.getSize();
}
@Override
public boolean isEmpty(){
return list.isEmpty();
}
@Override
public boolean contains(E e){
return list.contains(e);
}
@Override
public void add(E e){
if (!list.contains(e)){
list.addFirst(e);
}
}
@Override
public void remove(E e){
list.removeElement(e);
}
接下来我们基于昨天的二分搜索树,来实现Set的功能。具体如下:
public class BSTSet<E extends Comparable<E>> implements Set<E> {
private BST<E> bst;
public BSTSet(){
bst = new BST<>();
}
@Override
public int getSize(){
return bst.size();
}
@Override
public boolean isEmpty(){
return bst.isEmpty();
}
@Override
public void add(E e){
bst.add(e);
}
@Override
public boolean contains(E e){
return bst.contains(e);
}
@Override
public void remove(E e){
bst.remove(e);
}
}
接下来我们分析下二者在时间复杂度上的区别。
首先对于链表来说,其添加元素add时,需要判断是否包含contains,所以其时间复杂度为O(n),删除操作也为O(n)。
那么对于二分搜索树来说,假如h为二分搜索树的高度,无论添加、删除、查找操作,时间复杂度都为O(h)。
接下来我们就研究下高度h和节点数n的关系,对于二分搜索树来说,其h层的元素个数最多为2^(h-1)个,那么一个h层的满二叉树其元素个数即为:
n = 2^0+2^1+2^2+...+2^(h-1)=2^h-1,用对数来表示就是:h=log(n+1)。那么用大O表示法就是O(h)=O(logn)。
对于二叉树来说,其最坏的情况就是退化为链表的形式,其最坏时间复杂度为O(n)。小伙伴们有兴趣的话,可以计算下,当n逐渐变大时,O(logn)和O(n)的区别。
当然,我写了个小脚本,来测验二者的区别:
public 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) / 1000000000.0;
}
计算结果如下:
Total different words: 6530
BST Set: 0.1222529s
Total different words: 6530
Linked List Set: 2.1274651s
由此可以看出,用二叉树的效率比链表要快很多很多。同时,当时间复杂度涉及到log时,基本都是与树有关。
现在我们来看看映射Map,对于Map来说,它是一种存储(键, 值)数据对的数据结构(key, value)。比如x=f(n)这样的映射关系。日常使用中,我们会根据键(key),来快速查询值(value)。
同样,我们先用链表来实现映射Map,具体功能如下:
void add(k, v)
V remove(k)
boolean contains(k)
V get(k)
void set(k)
int getSize()
boolean isEmpty()
限于篇幅,我就把重要的功能实现代码展示一波:
private Node getNode(K key) {
Node cur = dummyHead.next;
while (cur != null) {
if (cur.key.equals(key)) {
return cur;
}
cur = cur.next;
}
return null;
}
@Override
public boolean contains(K key) {
return getNode(key) != null;
}
@Override
public V get(K key) {
Node node = getNode(key);
return node == null ? null : node.value;
}
@Override
public void add(K key, V value) {
Node node = getNode(key);
if (node == null) {
dummyHead.next = new Node(key, value, dummyHead.next);
size++;
} else {
node.value = value;
}
}
@Override
public void set(K key, V newValue) {
Node node = getNode(key);
if (node == null) {
throw new IllegalArgumentException(key + " doesn't exist!");
}
node.value = newValue;
}
@Override
public V remove(K key) {
Node prev = dummyHead;
while (prev.next != null) {
if (prev.next.key.equals(key)) {
break;
}
prev = prev.next;
}
if (prev.next != null) {
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
size--;
return delNode.value;
}
return null;
}
整体基于链表来实现,设定虚拟头结点dummyHead,也算是又复习了一波链表。
下面来看基于二分搜索树实现的Map。
// 向二分搜索树中添加新的元素(key, value)
@Override
public void add(K key, V value){
root = add(root, key, value);
}
// 向以node为根的二分搜索树中插入元素(key, value),递归算法
// 返回插入新节点后二分搜索树的根
private Node add(Node node, K key, V value){
if(node == null){
size ++;
return new Node(key, value);
}
if(key.compareTo(node.key) < 0) {
node.left = add(node.left, key, value);
} else if(key.compareTo(node.key) > 0) {
node.right = add(node.right, key, value);
} else // key.compareTo(node.key) == 0
{
node.value = value;
}
return node;
}
// 删除元素
private Node remove(Node node, K key){
if( node == null ) {
return null;
}
if( key.compareTo(node.key) < 0 ){
node.left = remove(node.left , key);
return node;
}
else if(key.compareTo(node.key) > 0 ){
node.right = remove(node.right, key);
return node;
}
else{ // key.compareTo(node.key) == 0
// 待删除节点左子树为空的情况
if(node.left == null){
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}
// 待删除节点右子树为空的情况
if(node.right == null){
Node leftNode = node.left;
node.left = null;
size --;
return leftNode;
}
// 待删除节点左右子树均不为空的情况
// 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
// 用这个节点顶替待删除节点的位置
Node successor = minimum(node.right);
successor.right = removeMin(node.right);
successor.left = node.left;
node.left = node.right = null;
return successor;
}
}
基本套用二分搜索树的逻辑,只是现在每个节点放的是(key, value)形式的数据,然后我们比较key值的大小,来实现左右子树的相关逻辑。
同样,我们最后来对比二者的时间复杂度,基于对链表的熟悉,我们知道无论是添加add、删除remove、修改set、查询contains等操作,其时间复杂度均为O(n)。而根据上述对于树结构的时间复杂度来看,其相关操作均为O(logn)。
OK,今天关于集合Set和映射Map的总结到这里,其实呢,主要是对比了链表和二叉树的时间复杂度……
那么,涉及到时间复杂度包含log的,基本都使用到树结构。另外,对于Set和Map,更多的东西,后续我会更新哈希表相关的知识点。
好了,今天就到这里。
拜了个拜~