目录
一、基于二分搜索树实现集合Set
集合set中不能存放重复元素
基于我们之前实现的二分搜索树BST,这里我们使用它来实现集合set。注意,之前我们构造二分搜索树这种数据结构时,我们定义是不存放重复元素的。
首先我们定义一下集合的通用接口
public interface Set<E> {
void add(E e);
void remove(E e);
boolean contains(E e);
int getSize();
boolean isEmpty();
}
根据我们之前实现的二分搜索树BST,实现集合代码如下:
public class BSTSet<E extends Comparable<E>> implements Set {
private BST<E> bst;
public BSTSet(){
bst = new BST<E>();
}
@Override
public void add(Object e) {
bst.add((E) e);
}
@Override
public void remove(Object e) {
bst.remove((E) e);
}
@Override
public boolean contains(Object e) {
return bst.contains((E) e);
}
@Override
public int getSize() {
return bst.size();
}
@Override
public boolean isEmpty() {
return bst.isEmpty();
}
}
二、基于链表实现集合Set
集合的底层实现可以使用不同的数据结构,此小结,我们基于之前实现的链表LinkedList,来实现一个set集合。下一小节会对比下之前通过二分搜索树实现的集合,看看他们在性能上的差异。
当然在之前实现的链表LinkedList中,我们需要补上删除任意元素方法的代码,如下:
// 从链表中删除元素
public void removeElement(E e){
Node pre = dummyHead;
while(pre.next != null){
if(pre.next.e.equals(e)){
break;
}
pre = pre.next;
}
if(pre.next != null){
// 解除连之间的关系
Node delNode = pre.next;
pre.next = delNode.next;
delNode.next = null;
}
}
那么通过链表来实现set集合,我们的代码如下:
public class LinkedListSet<E> implements Set<E> {
private LinkedList<E> list;
public LinkedListSet(){
list = new LinkedList<E>();
}
@Override
public void add(E e) {
if (!list.contains(e)) {
// O(1)级别的方法
list.addFirst(e);
}
}
@Override
public void remove(E e) {
list.removeElement(e);
}
@Override
public boolean contains(E e) {
return list.contains(e);
}
@Override
public int getSize() {
return list.getSize();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
}
三、集合的时间复杂度分析
为了测试LinkedSet和BSTSet的性能差异,我们编写了一个简单的测试程序:
public static void main(String[] args) {
int opCount = 3000;
BSTSet<Integer> bstSet = new BSTSet<Integer>();
double time1 = testSet(bstSet, opCount);
System.out.println("bstSet :" + time1 + " s");
LinkedListSet<Integer> linkedListSet = new LinkedListSet<Integer>();
double time2 = testSet(linkedListSet, opCount);
System.out.println("linkedListSet :" + time2 + " s");
}
private static double testSet(Set<Integer> set, int count) {
long startTime = System.nanoTime();
Random random = new Random();
for (int i = 0; i < count; i++) {
// 插入一个从0到int最大数之间的一个随机值,这里随机很重要
set.add(random.nextInt(Integer.MAX_VALUE));
}
long endTime = System.nanoTime();
// 以秒为单位
return (endTime - startTime) / 1000000000.0;
}
测试结果
从随机插入值的情况来看,BSTSet的运行结果要比LinkedSet的运行效率要高,这是因为BSTSet增、删、查的时间复杂度要小于LinkedSet的时间复杂度。如下图,BSTSet查找的次数,是BST树的深度。而LinkedSet在增、删、查的时候都需要去遍历整个集合,很明显,这种遍历次数的对比,就是存放数据部分值和总体值的对比。
那怎么来计算BSTSet的部分(h)和LinkedSet的整体(n)之间到底有多大的差距呢?这其实就是求解二分搜索树h层对应可以容纳多少元素的问题,我们假设元素的总数为n,我们可以求得h和n之间的关系:
我们得出的BSTSet和LinkedSet的时间复杂度如下:
基于两者的时间复杂度对比,上边的结论可能会有点抽象。下面,我们根据具体的数字来感觉一下他们之间的差异
但是,相同的元素在二分搜索树中会有不同的排列,当存储元素时,如果所有的元素都是事先排好顺序的,那么此时二分搜索树的深度就是元素的个数,即 h = n ;这个时候的二分搜索树被退化成一个链表,整棵树的节点都没有左孩子,它时间复杂度为O(n),这是二分搜索树最坏的情况。
集合的总结
集合可以分为有序集合和无序集合,上边通过二分搜索树实现的集合是一个有序的集合,因为涉及到排序,所以总体上来说,有序集合要比无序集合具备更多的性能开销。通过链表实现的集合是一个无序的集合,链表集合里边存储的元素是没有顺序的。不过,上边我们通过链表实现的无序集合是一个比较低效的无序集合,真正高效的无序集合,我们往往使用哈希表进行实现。
四、映射Map
所谓的映射就是一种以键值对形式存储数据的数据结构(key,value),即根据key去映射一个相对应的值value。
下边,我们将根据Map的这一结构特点,分别使用二分搜索树和链表来进行实现
首先,我们定义好一个Map的接口
public interface Map<K,V> {
void add(K key,V value);
V remove(K key);
boolean contains(K key);
V get(K key);
void set(K key,V newValue);
int getSize();
boolean isEmpty();
}
1、基于链表实现的Map
使用链表为底层结构的映射类,代码实现逻辑为下
public class LinkedListMap<K,V> implements Map<K,V>{
private class Node{
public K key;
public V value;
public Node next;
public Node(K key, V value, Node next){
this.key = key;
this.value = value;
this.next = next;
}
public Node(K key){
this(key,null,null);
}
public Node(){
this(null,null,null);
}
@Override
public String toString(){
return key.toString() + ":" +value.toString();
}
}
// 虚拟头节点
private Node dummyHead;
private int size;
public LinkedListMap(){
dummyHead = new Node();
size = 0;
}
@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 V remove(K key) {
Node pre = dummyHead;
while(pre.next != null){
if(pre.next.key.equals(key)){
break;
}
pre = pre.next;
}
if(pre.next != null){
Node delNode = pre.next;
pre.next = delNode.next;
delNode.next = null;
size --;
return delNode.value;
}
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 set(K key, V newValue) {
Node node = getNode(key);
if(node == null){
throw new IllegalArgumentException(key +"doesn't exits!");
}
node.value = newValue;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
// 辅助函数
private Node getNode(K key){
Node cur = dummyHead.next;
while(cur != null){
if(cur.key.equals(key)){
return cur;
}
cur = cur.next;
}
return null;
}
}
2、基于二分搜索树实现Map
使用二分搜索树,作为映射的底层数据结构,其实现逻辑和代码如下:
public class BSTMap<K extends Comparable<K>,V> implements Map<K,V> {
// 因为要存储键值对,所以不能完全复用之前的逻辑
private class Node{
public K key;
public V value;
public Node left,right;
public Node(K key,V value){
this.key = key;
this.value = value;
this.left = null;
this.right = null;
}
}
private Node root;
private int size;
public BSTMap(){
root = null;
size = 0;
}
@Override
public void add(K key, V value) {
root = add(root,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{
node.value = value;
}
return node;
}
@Override
public boolean contains(K key) {
return getNode(root,key) != null;
}
@Override
public V get(K key) {
Node node = getNode(root, key);
return node == null ? null : node.value;
}
@Override
public void set(K key, V newValue) {
Node node = getNode(root,key);
if(node == null){
throw new IllegalArgumentException(key +" is Empty !");
}
node.value = newValue;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
// 辅助函数
private Node getNode(Node node,K key){
if(node == null){
return null;
}
if(key.compareTo(node.key) == 0){
return node;
}else if(key.compareTo(node.key) < 0){
return getNode(node.left,key);
}else{
return getNode(node.right,key);
}
}
private Node minimum(Node node){
if(node.left == null){
return node;
}
return minimum(node.left);
}
private Node removeMin(Node node) {
// 递归到底的逻辑
if (node.left == null) {
// 保留右子树
Node rightNode = node.right;
// 删除最小值,此时的右子树成为新的节点
node.right = null;
size--;
return rightNode;
}
node.left = removeMin(node.left);
return node;
}
// 删除Map中任意元素
@Override
public V remove(K key) {
Node node = getNode(root, key);
if(node != null){
// 删除元素
root = remove(root, key);
return node.value;
}
return null;
}
private Node remove(Node node, K key) {
// 二分搜索树为空
if (node == null) {
return null;
}
if (key.compareTo(node.key) > 0) {
// 大于根节点,从右子树删除
node.right = remove(node.right, key);
return node;
} else if (key.compareTo(node.key) < 0) {
// 小于根节点,从左子树删除
node.left = remove(node.left, key);
return node;
} else { // 等于根节点
// 如果左子树为空,直接用右子树来替换
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;
}
}
}
五、Map的时间复杂度分析
上边我们分别通过链表和二分搜索树作为底层数据结构来实现了两个Map,即LinkedListMap和BSTMap,下边我们写一个简单的测试程序,测试一下两者之间的性能差异,程序代码如下:
public static void main(String[] args) {
int opCount = 20000;
BSTMap<String,Object> bstMap = new BSTMap<String,Object>();
double time1 = testSet(bstMap, opCount);
System.out.println("bstMap :" + time1 + " s");
LinkedListMap<String,Object> linkedListMap = new LinkedListMap<String,Object>();
double time2 = testSet(linkedListMap, opCount);
System.out.println("linkedListMap :" + time2 + " s");
}
private static double testSet(Map<String,Object> map, int count) {
long startTime = System.nanoTime();
Random random = new Random();
for (int i = 0; i < count; i++) {
map.add("key"+random.nextInt(Integer.MAX_VALUE),"value"+random.nextInt(Integer.MAX_VALUE));
}
long endTime = System.nanoTime();
// 以秒为单位
return (endTime - startTime) / 1000000000.0;
}
测试结果如下:
我们可以看到,在2万次新增数据的情况下,bstMap的速度明显要快于LinkedListMap,其实这个问题跟上边集合的时间复杂度的问题是一样的,其本质就是LinkedList和BST遍历深度的问题,两者的时间复杂度对比如下:
有序映射和无序映射
集合和映射的关系
上边,通过尝试用链表和二分搜素树来实现集合和映射的过程,我们可以发现集合和映射其实存在很多的相似之处,可以这么说,映射就是在集合的基础上,每个Key多带了一个Value而已。而集合无非就是每个元素(key),它的Value为空的映射。
因此,对于集合和映射,我们可以这样来看,两者的区别就是元素存储样式的区别:一个存储单独元素,一个存储键值对元素。