java二叉搜索树、Map、Set详解用法和区别

  • 1. 搜索树
  • 2. Map
  • 3. Set
  • 4. OJ练习题

1. 搜索树

TreeMap和TreeSet底层是用一颗搜索树实现的,不过这颗树是一颗红黑树,树的节点有黑色也有红色,我们现在只需要先了解一下什么是搜索树。

java二叉搜索树、Map、Set详解用法和区别_第1张图片

二叉搜索树:又称二叉排序树

  1. 若它的左子树不为空,则左子树所有节点的值都小于根节点值
  2. 若它的右子树不为空,则右子树所有节点的值都大于根节点值
  3. 左右子树也是一颗二叉搜索树。
    java二叉搜索树、Map、Set详解用法和区别_第2张图片

查找:

  1. 若根节点不为空:
  • 若根节点key == 查找key 返回true;

  • 若根节点key > 查找key 在其左子树查找;

  • 若根节点key < 查找key 在其右子树查找;

  • 否则返回 false。

    public Node search(int key) {
                    Node cur = root;
                    while (cur != null) {
                            //1.若根节点key == 查找key,返回
                            if (key == cur.key) {
                                    return cur;
                            //2.若根的key大于查找key去左子树
                            } else if (cur.key > key) {
                                    cur = cur.left;
                            //3.若根的key大小于查找key去右子树
                            } else {
                                    cur = cur.right;
                            }
                    }
                    //4.找不到返回null
                    return null;
            }
    

插入:

分情况: 每次插入都是从根节点往下遍历

  1. 若该树为空树,即:根(root) == null;直接插入元素。
  2. 若树不为空树,按照查找逻辑,找到合适位置,插入新节点。
    • 如:插入10,此时根节点是5,10比5大,往5的右子树遍历;
    • 此时根节点是8,10比8大,往8的右子树遍历;
    • 此时根节点是12,10比12小,往12的左子树遍历。
    • 此时根节点为null,直接插入。
    • java二叉搜索树、Map、Set详解用法和区别_第3张图片
   public boolean insert(int key) {
                //1.若根节点是null,直接插入
                if (root == null) {
                        root = new Node(key);
                        return true;
                }
                Node cur = root;
                Node parent = null;
                //2.遍历找到待插入key的合适位置
                //待插入key,插入的位置肯定是一个根节点的叶子节点
                //当cur为null,parent就是待插入key的根节点
                while (cur != null) {
                        if (key == cur.key) {
                                return false;
                        } else if (key < cur.key) {
                                parent = cur;
                                cur = cur.left;
                        } else {
                                parent = cur;
                                cur = cur.right;
                        }
                }
                //3.判断待插入key比当前节点key的大小关系
                //待插入key比根key大,就插入根节点右子树
                //否则插入根节点左子树
                Node node = new Node(key);
                if (key < parent.key) {
                        parent.left = node;
                } else {
                        parent.right = node;
                }
                return true;
        }

删除:

分情况: 假设待删除节点为cur,待删除节点的父亲节点为parent。

  1. cur.left == null;

    • cur 不是根节点(root),则根节点 == cur.right;

      java二叉搜索树、Map、Set详解用法和区别_第4张图片

    • cur 不是根节点(root),cur是parent的left,则parent.left = cur.right;

      java二叉搜索树、Map、Set详解用法和区别_第5张图片

    • cur 不是根节点(root),cur是parent.right,则parent.right = cur.right。

    java二叉搜索树、Map、Set详解用法和区别_第6张图片

  2. cur.right == null

  • cur是根节点(root),则root = cur.left;

java二叉搜索树、Map、Set详解用法和区别_第7张图片

  • cur不是根节点(root),cur是parent.left,则parent.left = cur.left;

    java二叉搜索树、Map、Set详解用法和区别_第8张图片

  • cur不是根节点(root),cur 是 parent.right,则 parent.right = cur.left

    java二叉搜索树、Map、Set详解用法和区别_第9张图片

  1. cur.left != null && cur.right != null

    需要使用一种替罪羊法来进行删除,即在当前要删除的节点的左子树的最右边找叶子节点(找到9),或者当前删除节点的右子树的最左边找叶子节点(找到16).

    • 假设要删除的节点是15,把15和9交换,或者把15和16交换。

    • 9:一定是当前删除节点左树中的最大值。(既然是最大值,9肯定是没有右子树了)

    • 16:一定是当前删除节点右树中的最小值。(既然是最小值,16肯定是没有左子树了)

    • **注意:**要判断待删除节点的左子树的有无右子树/(右子树有无左子树)。

    • 此时问题就转换为如何删除9或者16:

      1. cur.key = t.key;

      2. tp.left = t.rigth;

    • java二叉搜索树、Map、Set详解用法和区别_第10张图片

java二叉搜索树、Map、Set详解用法和区别_第11张图片

  public boolean remove(int key) {
                Node cur = root;
                Node parent = null;
                //1.遍历找到要删除key的位置
                while (cur != null) {
                        if (cur.key == key) {
                                removeHelp(parent, cur);
                                return true;
                        } else if (cur.key > key) {
                                parent = cur;
                                cur = cur.left;
                        } else if (cur.key < key) {
                                parent = cur;
                                cur = cur.right;
                        }
                }
                return false;
        }
 private void removeHelp(Node parent,Node cur){
                if(cur.left == null){

                        if(cur == root){
                                root = cur.right;
                        } else if (cur == parent.left) {
                                parent.left = cur.right;
                        }else if(cur == parent.right){
                                parent.right = cur.right;
                        }

                }else if(cur.right == null){

                        if(cur == root){
                                root = cur.left;
                        } else if (cur == parent.left) {
                                parent.left = cur.left;
                        }else if(cur == parent.right){
                                parent.right = cur.left;
                        }

                }else {
                        //1.从待删除节点的右子树的最左边找替罪羊
                        Node target = cur.right;//替罪羊节点
                        Node targetParent = cur;
                        while(target != null){
                                targetParent = target;
                                target = target.left;
                        }
                        //2.交换值
                        cur.key = target.key;
                        //3.判断,
                        //待删除节点的右树有没有左子树
                        if(target == targetParent.left){
                                targetParent.left = target.right;
                        }else {
                                targetParent.right = target.right;
                        }
                }

结论:

  1. 插入和删除操作都必须先查找,即查找就代表了二叉搜索树的各个操作的性能。

  2. 对有N个节点的二叉搜索树,若每个元素查找的概率相等(满二叉树),则平均查找长度是节点在二叉搜索树的深度的函数(logn),即节点越深,比较次数越多。

  3. 对于同一组数据,如果插入的次序不同,可能得到不同结构的二叉搜索树。

    1. 最优情况:二叉搜索树为完全二叉树,平均比较次数为O(logN)。

    2. 最差情况:二叉搜索树退化为单支树,其平均比较次数为O(N)。
      java二叉搜索树、Map、Set详解用法和区别_第12张图片

    3. TreeMap 和 TreeSet 即 java 中利用搜索树实现的 Map 和 Set;实际上用的是红黑树,而红黑树是一棵近似平衡的 二叉搜索树,即在二叉搜索树的基础之上 + 颜色以及红黑树性质验证,关于红黑树的内容这里不做讲解。

      简单实现二叉搜索树:

       
       public class BinarySearchTree {
               public static class Node {
                       int key;
                       Node left;
                       Node right;
       
                       public Node(int key) {
                               this.key = key;
                       }
               }
       
               private Node root = null;
       
               public Node search(int key) {
                       Node cur = root;
                       while (cur != null) {
                               //1.若根节点,返回
                               if (key == cur.key) {
                                       return cur;
                                       //2.若根的key大于查找key去左子树
                               } else if (cur.key > key) {
                                       cur = cur.left;
                                       //3.若根的key大小于查找key去右子树
                               } else {
                                       cur = cur.right;
                               }
                       }
                       //4.找不到返回null
                       return null;
               }
       
               public boolean insert(int key) {
                       //1.若根节点是null,直接插入
                       if (root == null) {
                               root = new Node(key);
                               return true;
                       }
                       Node cur = root;
                       Node parent = null;
                       //2.遍历找到待插入key的合适位置
                       //待插入key,插入的位置肯定是一个根节点的叶子节点
                       //当cur为null,parent就是待插入key的根节点
                       while (cur != null) {
                               if (key == cur.key) {
                                       return false;
                               } else if (key < cur.key) {
                                       parent = cur;
                                       cur = cur.left;
                               } else {
                                       parent = cur;
                                       cur = cur.right;
                               }
                       }
                       //3.判断待插入key比当前节点key的大小关系
                       //待插入key比根key大,就插入根节点右子树
                       //否则插入根节点左子树
                       Node node = new Node(key);
                       if (key < parent.key) {
                               parent.left = node;
                       } else {
                               parent.right = node;
                       }
                       return true;
               }
       
               public boolean remove(int key) {
                       Node cur = root;
                       Node parent = null;
                       //1.遍历找到要删除key的位置
                       while (cur != null) {
                               if (cur.key == key) {
                                       removeHelp(parent, cur);
                                       return true;
                               } else if (cur.key > key) {
                                       parent = cur;
                                       cur = cur.left;
                               } else if (cur.key < key) {
                                       parent = cur;
                                       cur = cur.right;
                               }
                       }
                       return false;
               }
               private void removeHelp(Node parent,Node cur){
                       if(cur.left == null){
       
                               if(cur == root){
                                       root = cur.right;
                               } else if (cur == parent.left) {
                                       parent.left = cur.right;
                               }else if(cur == parent.right){
                                       parent.right = cur.right;
                               }
       
                       }else if(cur.right == null){
       
                               if(cur == root){
                                       root = cur.left;
                               } else if (cur == parent.left) {
                                       parent.left = cur.left;
                               }else if(cur == parent.right){
                                       parent.right = cur.left;
                               }
       
                       }else {
                               //1.从待删除节点的右子树的最左边找替罪羊
                               Node target = cur.right;//替罪羊节点
                               Node targetParent = cur;
                               while(target != null){
                                       targetParent = target;
                                       target = target.left;
                               }
                               //2.交换值
                               cur.key = target.key;
                               //3.判断,
                               //待删除节点的右树有没有左子树
                               if(target == targetParent.left){
                                       targetParent.left = target.right;
                               }else {
                                       targetParent.right = target.right;
                               }
                       }
               }
       
       
       
       }

2. Map

java二叉搜索树、Map、Set详解用法和区别_第13张图片

1.Map和set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关

  • 直接遍历:时间复杂度O(N)
  • 二分查找:时间复杂度O(logN),但搜索前必须要求数据是有序的。
  • 比较适合静态类型查找,一般不会对指定区间进行插入和删除。

2.Map:是一个接口类,该类没有继承自Collection,该类中存储的是结构的键值对,并且K一定是唯一的,不能重复,但是V可以重复(修改原本K的V值)。

  • K和V的类型可以相同,也可以不同,K的类型一定要是可以比较的

  • 键值对:表示K和V是一一对应的关系,比如K=面包,V=10(元)。

Map常用方法:

方法 解释
V get(Object key) 返回 key 对应的 value
V getOrDefault(Object key, V defaultValue) 返回 key 对应的 value,key 不存在,返回默认值
V put(K key, V value) 设置 key 对应的 value
V remove(Object key) 删除 key 对应的映射关系
Set keySet() 返回所有 key 的不重复集合
Collection values() 返回所有 value 的可重复集合
Set> entrySet() 返回所有的 key-value 映射关系
boolean containsKey(Object key) 判断是否包含 key,有返回true,否则false
boolean containsValue(Object value) 判断是否包含 value,有返回true,否则false
  1. Map是一个接口,不能直接实例化对象,如果要实例化对象只能通过实例化实现类TreeMap或HashMap。

  2. HashMap :存储数据采用哈希表结构,元素的存取顺序不能保证一致,K的类型一定要可以比较,由于要保证K的唯一,不重复,需要重写HashCode()、equals()方法。

  3. 使用put方法时,如传的key存在,则只会把指定key所对应的value值,替换成指定的新值,而不会再添加一个key,返回值为key对应的value值。

  4. 使用get方法时,获取指定key所对应的value值。

  5. remove方法,根据指定的key删除元素,返回被删除元素的value值。

  6. TreeMap和HashMap的区别:

    Map底层结构 TreeMap HashMap
    底层结构 红黑树 哈希桶
    插入/删除/查找时间 复杂度 O(logN) O(1)
    是否有序 关于Key有序 无序
    线程安全 不安全 不安全
    插入/删除/查找区别 需要进行元素比较 通过哈希函数计算哈希地址
    比较与覆写 key必须能够比较,否则会抛出 ClassCastException异常 自定义类型需要覆写equals和 hashCode方法
    应用场景 需要Key有序场景下 Key是否有序不关心,需要更高的 时间性能

    HashMap方法演示:

    public class Main {
        public static void main(String[] args) {
            //创建HashMap对象,key为String类型,value为Integer
            HashMap<String,Integer> map = new HashMap<>();
           //给map添加元素,每一次放入都会进行比较,第一次放入也会比较
            map.put("奶茶",18);
            map.put("面包",6);
            map.put("咖啡",22);
            map.put(null,null);
            //因为value是Integer类型,可以使用int也可以Integer接收
            //这涉及到拆包问题
            int get1 = map.get("奶茶");//返回18
            Integer get2 = map.get("咖啡");//返回22
            //因为HashMap重写了toString
            //输出:{null=null, 面包=6, 咖啡=22, 奶茶=18}
            System.out.println(map);
            
            Integer del = map.remove("奶茶");//返回对应value值
            
        }
    }
    

``

Set> entrySet():集合遍历键值対

Map.Entry是Map内部实现用来存放键值対映射关系的内部类,其中主要提供了的获取,value的设置以及key的比较方式。

方法 解释
K getKey() 返回 entry 中的 key
V getValue() 返回 entry 中的 value
V setValue(V value) 将键值对中的value替换为指定value

获取Map集合中,所有的Key和Value并组织起来,把相对的K和V作为一个整体,放入Set,组织起来的这个结构就叫做Map.Entry。原先Key,Value的类型是Map,现在变成了Map.Entry类型。(泛型的参数,不参加类型的组成的:ArrayList < String> list1 和Arra< Integer > list2,此时list1和list2的类型都是ArrayList)

java二叉搜索树、Map、Set详解用法和区别_第14张图片

源码:
java二叉搜索树、Map、Set详解用法和区别_第15张图片

使用方法:

可以直接通过for each 遍历原先的Map里的内容。如果不实现Set> entrySet(),那么是无法遍历得到Map里的key和value值的。

java二叉搜索树、Map、Set详解用法和区别_第16张图片

3. Set

Set的官方文档

常见方法:

方法 解释
boolean add(E e) 添加元素,但重复元素不会被添加成功
void clear() 清空集合
boolean contains(Object o) 判断 o 是否在集合中
Iterator iterator() 返回迭代器
boolean remove(Object o) 删除集合中的o
int size() 返回set中元素的个数
boolean isEmpty() 检测set是否为空,空返回true,否则返回false
Object[] toArray() 将set中的元素转换为数组返回
boolean containsAll(Collection c) 集合c中的元素是否在set中全部存在,是返回true,否则返回 false
boolean addAll(Collection c) 将集合c中的元素添加到set中,可以达到去重的效果

注意:

  1. Set是继承Collection的一个接口类。
  2. Set中只存储了key,并且要求key一定要唯一。
  3. Set的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的。
  4. Set最大的功能就是对集合中的元素进行去重。
  5. 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础 上维护了一个双向链表来记录元素的插入次序。
  6. Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入。
  7. Set中不能插入null的key。

TreeSet和HashSet的区别:

Set底层结构 TreeSet HashSet
底层结构 红黑树 哈希桶
插入/删除/查找时间 复杂度 O(logN) O(1)
是否有序 关于Key有序 不一定有序
线程安全 不安全 不安全
插入/删除/查找区别 按照红黑树的特性来进行插入和删除 1.先计算key哈希地址 2. 然后进行 插入和删除
比较与覆写 key必须能够比较,否则会抛出 ClassCastException异常 自定义类型需要覆写equals和 hashCode方法
应用场景 需要Key有序场景下 Key是否有序不关心,需要更高的 时间性能
public class Main {
    public static void main(String[] args) {
        HashSet<String> hashSet = new HashSet<>();
        //add(key),如果不存在,则插入并返回true
        //key为空,报空指针异常
        boolean insert = hashSet.add("咖啡");
        hashSet.add("拿铁");

        //contains(key),如果key存在,返回true
        boolean cont = hashSet.contains("咖啡");
        //remove(key),如果key存在删除返回true
        //key为空,报空指针异常
        boolean del = hashSet.remove("咖啡");
        
        //使用迭代器遍历
        Iterator<String> it = hashSet.iterator();
        while(it.hasNext()){
            System.out.println(it.next()+" ");
            //程序运行,输出 拿铁
            //咖啡 已被删除了
        }
    }
}

4. OJ练习题

  1. 只出现一次的数字

  2. 宝石与石头

  3. 坏键盘打字

  4. 前K个高频单词

  5. 复制带随机指针的链表

你可能感兴趣的:(数据结构,java,数据结构,算法,Map,Set)