【数据结构与算法】第七篇:集合,映射

一.集合

1、集合的特点

◼ 集合的特点
不存放重复的元素
对于集合它的主要用途是常用于去重。
集合我们主要记住它不存放重复元素的特点,根据此特点又细化了出许多具体的功能:
1.✓ 存放新增 IP,统计新增 IP 量
2.✓ 存放词汇,统计词汇量

2、集合的接口设计

对于集合接口我们主要是维护它的遍历方法以及访问器。
除此以外在实现集合各个方法是一定要维护它的不重复性

public interface Set<E> {
	int size();	//元素数量
	boolean isEmpty(); // 是否为空
	void claer(); // 清空集合
	boolean contains(E element); // 是否包含element元素
	void add(E element); // 添加element元素
	void remove(E element); // 删除element元素
	void traversal(Visitor<E> visitor); // 通过访问器遍历
	
	public static abstract class Visitor<E>{ // 访问器
		boolean stop;
		public abstract boolean visit(E element);
	}
}

3.链表实现集合(listSet)

package set;

import list.LinkedList;
import list.List;

public class listSet<E>implements Set<E>{
    private final List<E> list=new LinkedList<>();
    public int size(){
        return list.size();
    }
    public boolean isEmpty(){

        return list.isEmpty();
    }
    public void clear(){
        list.clear();
    }
    public boolean contains(E element){
        return list.contains(element);
    }
    //这里就不能写List.add(element)因为集合不能有重复的元素
    //这样写就默认重复的元素可有加进去了
   public  void add(E element){
        if(list.contains(element)){
            return;
        }
        list.add(element);
       /**
        *int index=list.indexOf(element);
        *        if(index!=List.ELEMENT_NOT_FOUND){
        *            list.set(index,element);
        *        }else {
        *            list.add(element);
        *        }
        */

   }
   public void remove(E element){
        if(list.indexOf(element)!=List.ELEMENT_NOT_FOUND) {
            list.remove(list.indexOf(element));
        }
    }

    public void traversal(Visitor<E> visitor) {
        if(visitor==null)
        {
            return;
        }
        int n=list.size();
        for (int i = 0; i <n ; i++) {
            if(visitor.visit(list.get(i))){
                return;
            }
        }
    }
}

4.红黑树实现集合(TreeSet)

用红黑树实现集合有一个局限性,红黑树中的元素必须具有可比较性,所以最好传入一个比较器comparator

package set;

import tree.BinaryTree;
import tree.RBTree;

import java.util.Comparator;
@SuppressWarnings("unchecked")
public class TreeSet<E>implements Set<E> {
    private  RBTree<E> tree=new RBTree<>();

    public TreeSet()
    {
        this(null);
    }

    //由于红黑树具有局限性-->数据必须具有比较性。所以最好传入一个比较器
    public TreeSet(Comparator<E> comparator){
        tree=new RBTree<>(comparator);

    }
    @Override
    public int size() {
        return tree.size();
    }

    @Override
    public boolean isEmpty() {
        return tree.isEmpty();
    }

    @Override
    public void clear() {
        tree.clear();
    }

    @Override
    public boolean contains(E element) {
        return tree.contains(element);
    }

    @Override
    public void add(E element) {
        tree.add(element);

    }

    @Override
    public void remove(E element) {
        tree.remove(element);
    }

    @Override
    public void traversal(Visitor<E> visitor) {
        tree.inorder(new BinaryTree.Visitor<E>() {
            @Override
            public boolean visit(E element) {
                //用现有Set理的visitor来控制限定条件
                return visitor.visit(element);
            }
        });
    }
}

二. 映射

1.映射的特点

映射( Map) 在有些编程语言中也叫做字典(dictionary,比如 Python、Objective-C、Swift 等)。
既然是字典的功能,那么必然会有两个值。key和value通过key(线索)来找到value。

在这里插入图片描述

2.映射的接口设计

public interface Map <k,v>{
//泛型实现两个类型
    int size();
    boolean isEmpty();
    void clear();
    v put(k key,v value);
    v get(k key);
    v remove(k key);
    boolean containsKey(k key);
    boolean containsValue(v value);
    void traversal(Visitor<k, v> visitor);
    public static abstract class Visitor<k, v> {
        boolean stop;
        public abstract boolean visit(k key, v value);
    }

}

2.红黑树实现映射

package map;

import java.util.Comparator;
import java.util.LinkedList;
import java.util.Queue;

//红黑树实现映射
//因为现在是两个泛型,红黑树每个节点都存储这两个数据
//如果还是继承以前的节点是不现实的--->所以这里我们需要自己构建一个红黑树
@SuppressWarnings("unchecked")
public class TreeMap <k,v>implements Map<k,v>{
    private static final boolean RED=true;
    private static final boolean BLACK=false;
    int size;
    Node<k,v> root;
   Comparator<k> comparator;
    //红黑树数据必须具有比较性所以传入构造器
    public TreeMap(){this(null);}
    public TreeMap( Comparator<k> comparator) {
        this.comparator=comparator;
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size==0;
    }

    @Override
    public void clear() {
        root=null;
        size=0;
    }

    private void checkIsNull(k key){
    if(key==null)
    {
        throw new UnsupportedOperationException("key不合法");
    }
    }
    @Override
    public v put(k key, v value) {
        checkIsNull(key);
        // 添加第一个节点
        if (root == null) {
            //一开始的写法是new Node<>(element,null);对应值和父节点

            root = createNode(key,value, null);
            size++;

            // 新添加节点之后的处理(巧妙之处---写在父类的add中追加判断调整)
            //如果主函数创建的是单纯的二叉搜索树则方法体是空不做处理,如果是AvL是则
            //遵循重写规则进行恢复
            afterPut(root);
            //这里的返回是返回它之前的节点,根节点之前没有节点
            return null;
        }

        // 添加的不是第一个节点
        // 找到父节点
        Node<k,v> parent = root;
        Node<k,v> node = root;
        int cmp = 0;
        do {
            cmp = compare(key ,node.key);
            parent = node;
            if (cmp > 0) {
                node = node.right;
            } else if (cmp < 0) {
                node = node.left;
            } else { // 相等,这里注意如果要是相等key,value都赋值给这个节点,返回老value
                //相等就覆盖
                node.key = key;
                v oldValue=node.value;//node被覆盖之前的的value;
                node.value=value;
                return oldValue;
            }
        } while (node != null);

        // 看看插入到父节点的哪个位置
        //到这里就是已经红黑树之前没有这个数据,这完全是新添加的节点,而且添加的位置就是node的位置
        Node<k,v> newNode = createNode(key,value, parent);
        if (cmp > 0) {
            parent.right = newNode;
        } else {
            parent.left = newNode;
        }
        size++;

        // 新添加节点之后的处理
        afterPut(newNode);

        return null;
    }
    private Node<k,v> createNode(k key,v value,Node<k,v> parent){
        return new Node<>(key,value,parent);
    }
    private void afterPut(Node<k,v> node)
    {
        Node<k,v> parent=node.parent;
        //这个代码块包括两种情况--->(1)第一次添加节点(2)叔父节点是红色的情况下,上溢到了根节点
        if(parent==null){
            black(node);
            root=node;
            return;
        }
        if(isBlack(parent))
        {
            //排除本身就满足条件的哪四种情况,不需要额外处理
            return;
        }
        Node<k,v> uncle=parent.sibling();
        Node<k,v> grand=parent.parent;
        //叔父节点是红色-->发生上溢
        if(isRed(uncle)){
            //上溢分裂的情况
            black(parent);
            black(uncle);
            //把祖父节点当成一个新添加的节点加到上面-->解决上溢
            afterPut(red(grand));
            return;
        }
        //叔父节点不是红节点
        if(parent.isLeftChild())
        {
            //由于染色顺序并不影响所以可以把统一的染色顺序放在前面red(grand)
            //LL RR 单旋
            if(node.isLeftChild()){//LL
                black(parent);
                red(grand);
            }else {//LR
                black(node);
                red(grand);
                rotateLeft(parent);
            }
            rotateRight(grand);
        }else{
            if(node.isRightChild())//RR
            {
                black(parent);
                red(grand);
            }else{//RL
                black(node);
                red(grand);
                rotateRight(parent);
            }
            rotateLeft(grand);
        }
    }
    private int compare(k e1,k e2){
        if (comparator != null) {
            return comparator.compare(e1, e2);
        }
        return ((Comparable<k>)e1).compareTo(e2);
    }

    private void rotateLeft(Node<k,v> grand) {
        Node<k,v> parent = grand.right;
        Node<k,v> child = parent.left;
        grand.right = child;
        parent.left = grand;
        afterRotate(grand, parent, child);
    }

    private void rotateRight(Node<k,v> grand) {
        Node<k,v> parent = grand.left;
        Node<k,v> child = parent.right;
        grand.left = child;
        parent.right = grand;
        afterRotate(grand, parent, child);
    }

    protected void afterRotate(Node<k,v> grand, Node<k,v> parent, Node<k,v> child) {
        // 让parent称为子树的根节点
        parent.parent = grand.parent;
        if (grand.isLeftChild()) {
            grand.parent.left = parent;
        } else if (grand.isRightChild()) {
            grand.parent.right = parent;
        } else { // grand是root节点
            root = parent;
        }

        // 更新child的parent
        if (child != null) {
            child.parent = grand;
        }

        // 更新grand的parent
        grand.parent = parent;
    }

    private Node<k,v> color(Node<k,v> node,boolean color){
        //代码习惯:染色的同时返回被染色的节点便于对其进行一些操作
        if(node==null)
        {
            return null;
        }
        node.color=color;
        return node;
    }
    public Node<k,v> red(Node<k,v> node)
    {
        return color(node,RED);
    }
    public Node<k,v> black(Node<k,v> node)
    {
        return color(node,BLACK);
    }

    //判断颜色
    private boolean iscolor(Node<k,v> node)
    {
        return node==null?BLACK:node.color;

    }
    public boolean isRed(Node<k,v> node)
    {
        return iscolor(node)==RED;

    }
    public boolean isBlack(Node<k,v> node){
        return iscolor(node)==BLACK;
    }


    @Override
    public v get(k key) {
        Node<k,v> node=node(key);
        return node!=null?node.value:null;
    }
    //给定key值删除
    @Override
    public v remove(k key) {
        return remove(node(key));
    }
    //给定节点删除
    private v remove(Node<k,v> node) {
        if (node == null) return null;
        size--;

        v old=node.value;
        if (node.hasTwoChildren()) { // 度为2的节点
            // 找到后继节点
            Node<k,v> s = successor(node);
            // 用后继节点的值覆盖度为2的节点的值
            node.key = s.key;
            node.value=s.value;
            // 删除后继节点
            node = s;
        }

        // 删除node节点(node的度必然是1或者0)
        Node<k,v> replacement = node.left != null ? node.left : node.right;

        if (replacement != null) { // node是度为1的节点
            replacement.parent=node.parent;
            if(node.parent==null){//node是度为一的根节点
                root=replacement;
            }else if(node==node.parent.left){
                node.parent.left=replacement;
            }else{
                node.parent.right=replacement;
            }
            afterRemove(node,replacement);

            //除了这个if代码块就都是叶子节点了
        } else if (node.parent == null) { // node是叶子节点并且是根节点
            root=null;
            afterRemove(node, null);
        } else { // node是叶子节点,但不是根节点
            if (node == node.parent.left) {
                node.parent.left=null;
            } else { // node == node.parent.right
                node.parent.right=null;
            }
            //细节:这里不管删除操作如何进行--都没由对node,parent进行销毁,parent仍然存在
            // 删除节点之后的处理
            afterRemove(node, null);
        }
        return old;
    }

    private Node<k,v> node(k element) {
        Node<k,v> node = root;
        while (node != null) {
            int cmp = compare(element, node.key);
            if (cmp == 0) return node;
            if (cmp > 0) {
                node = node.right;
            } else { // cmp < 0
                node = node.left;
            }
        }
        return null;
    }
    private void afterRemove(Node<k,v> node,Node<k,v> replacement){
        //b树类比红黑树一定是删除的最后一层。如果要删除的是红色,由于黑色是主体,
        // 所以要删除的节点一定度为0的红色叶子节点,不会影响原本红黑树的规则,所以不用调整,直接结束函数
        if(isRed(node)){
            return;
        }
        /**
         * 一定要清楚afterRemove的作用是删除之后的调整,不是主体删除方法。
         * 作用:对于不符合红黑树性质的操作及时进行调整
         *
         * 问:对于删除度为2的黑色节点,是不是可是囊括
         * 答:可以:因为删除度为2的黑色节点,找到前驱后,还是删除红色节点不用处理,染黑替代节点并不影响,因为已经删除
         *
         * 为什么黑色节点的替代不能为黑色节点
         * 因为b树的删除都是在最后一层进行操作,如果黑色节点子节点依然是一个黑色节点,由于黑色才是主体
         * 不会融入到一个节点中,所以只会向下增高一层,不符合删除在最后一层操作。,
         */
        if(isRed(replacement)){
            //染黑:避免出现连续两个红色节点
            black(replacement);
            return;
        }
        /**
         * 处理最后一种情况-->删除度为0的黑色叶子节点的调整方法
         *
         * 删除度为0的黑色叶子节点的调整方法肯定会造成下溢
         * 补救:(1)看看兄弟节点能不能借
         * a:兄弟节点能借的条件:1.兄弟节点是黑色 2.兄弟节点至少拥有一个红色子节点
         * ----------------------------------------
         * 能借后做出的操作:
         * 进行旋转操作
         * 旋转之后的中心节点继承 parent 的颜色
         * 旋转之后的左右节点染为 BLACK(因为此时红色子节点已经旋上去了)
         *
         * 旋转有三种情况:LL LR LL
         *
         * 🤩🤩特殊情况1:
         * 如果被删除的是黑色,它的父节点也是黑色,删除后黑色父节点也会向下合并,造成父节点继续下溢
         *只需将这个父节点当成一个新的被删除的黑色节点递归调用afterRemove方法,重复利用代码
         *
         * 🤩🤩特殊情况2:
         * 🚩🚩🚩如果兄弟节点是红色,对于b树的性质,【黑色节点与其红色节点组成一个b树节点】
         * 可知:红色节点一定在父节点中-->红色的节点的子节点一定是黑的,可以借侄子的【想办法将侄子变成兄弟】
         * 强制将侄子变成兄弟再次套用兄弟节点是黑色是的代码
         *-----------------------------------------------
         */

        Node<k,v> parent=node.parent;
        if(parent==null)
        {
            return;
        }
        //这么写有问题:node已经被删除--->指向node的线已经断了
        //Node<k,v> sibling =node.sibling();
        /**
         *  需要间接求出兄弟节点
         *  只需知道被删除的节点先前是在parent的左边还是右边,进而知道sibling是parent.left还是parent.right
         */

        /**
         *    为什么以此作为判定标准:
         *         因为在二叉搜索树的删除中,删除黑色叶子节点,已经将一边置为null
         *         if (node == node.parent.left) {
         *             node.parent.left=null;
         *         } else { // node == node.parent.right
         *             node.parent.right=null;
         *         }
         */
        //有特殊情况
        boolean left=parent.left==null||node.isLeftChild();
        Node<k,v> sibling=left?parent.right:parent.left;
        if(left){//node是在左子树,兄弟节点在右边
            if(isRed(sibling)){
                black(parent);
                red(sibling);
                rotateLeft(parent);
                //更新兄弟
                sibling=parent.right;
            }

            //兄弟节点是黑色-->判断是否有红色子节点

            //没有一个红色子节点--->父节点向下合并

            /**
             * 为什么以父节点是不是黑的为判定条件
             * 因为如果父节点是红色,那它肯定不是主体,向下合并后,parent本身并没有下溢
             * 如果父节点是黑色,那他向下合并后,自身也会下溢
             *
             */
            if(isBlack(sibling.left)&&isBlack(sibling.right)){
                boolean parentBlack=isBlack(parent);
                black(parent);
                red(sibling);
                //这种情况父节点向下合并,父节点的位置也会下溢
                if(parentBlack){
                    //三黑局面没有替代节点
                    afterRemove(parent,null);
                }

            }else {//至少有一个红色子节点
                //判断是LL LR.....
                //三种情况:LL LR (LL或LR)
                //可以先统一进行左旋转然后在统一进行右旋转
                //如何区分LR左子节点是黑的
                if(isBlack(sibling.left)) {//LR情况

                    rotateLeft(sibling);
                    //左旋转后兄弟节点已经发生了变化
                    sibling = parent.left;
                }
                //LL 情况
                //现在的sibling已经更新过是图中的78
                color(sibling,iscolor(parent));
                black(parent);
                black(sibling.left);
                rotateRight(parent);
            }

        }else {//node是在右子树,兄弟节点在左边
            //🤩🤩特殊情况2:
            if(isRed(sibling)){
                black(parent);
                red(sibling);
                rotateRight(parent);
                //更新兄弟
                sibling=parent.left;
            }

            //兄弟节点是黑色-->判断是否有红色子节点

            //没有一个红色子节点--->父节点向下合并

            /**
             * 为什么以父节点是不是黑的为判定条件
             * 因为如果父节点是红色,那它肯定不是主体,向下合并后,parent本身并没有下溢
             * 如果父节点是黑色,那他向下合并后,自身也会下溢
             *
             */
            if(isBlack(sibling.left)&&isBlack(sibling.right)){
                boolean parentBlack=isBlack(parent);
                black(parent);
                red(sibling);
                //这种情况父节点向下合并,父节点的位置也会下溢
                if(parentBlack){
                    //三黑局面没有替代节点
                    afterRemove(parent,null);
                }

            }else {//至少有一个红色子节点
                //判断是LL LR.....
                //三种情况:LL LR (LL或LR)
                //可以先统一进行左旋转然后在统一进行右旋转
                //如何区分LR左子节点是黑的
                if(isBlack(sibling.left)) {//LR情况

                    rotateLeft(sibling);
                    //左旋转后兄弟节点已经发生了变化
                    sibling = parent.left;
                }
                //LL 情况
                //现在的sibling已经更新过是图中的78
                color(sibling,iscolor(parent));
                black(parent);
                black(sibling.left);
                rotateRight(parent);
            }

        }
    }
    protected Node<k,v> predecessor(Node<k,v> node) {
        if (node == null) return null;

        // 前驱节点在左子树当中(left.right.right.right....)
        Node<k,v> p = node.left;
        if (p != null) {
            while (p.right != null) {
                p = p.right;
            }
            return p;
        }

        // 从父节点、祖父节点中寻找前驱节点
        while (node.parent != null && node == node.parent.left) {
            node = node.parent;
        }

        // node.parent == null
        // node == node.parent.right
        return node.parent;
    }

    protected Node<k,v> successor(Node<k,v> node) {
        if (node == null) return null;

        // 前驱节点在左子树当中(right.left.left.left....)
        Node<k,v> p = node.right;
        if (p != null) {
            while (p.left != null) {
                p = p.left;
            }
            return p;
        }

        // 从父节点、祖父节点中寻找前驱节点
        while (node.parent != null && node == node.parent.right) {
            node = node.parent;
        }

        return node.parent;
    }

    @Override
    public boolean containsKey(k key) {
        return node(key)!=null;
    }

    private boolean Equals(v v1,v v2){

        return v1==null?v2==null:v1.equals(v2);
    }
    @Override
    public boolean containsValue(v value) {
        if(root==null)
        {
            return false;
        }
        Queue<Node<k,v>> queue=new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty())
        {
            Node<k,v> node=queue.poll();
            boolean com=Equals(value,node.value);
            if(com)
            {
                return true;
            }

            if(node.left!=null)
            {
                queue.offer(node.left);
            }
            if(node.right!=null)
            {
                queue.offer(node.right);
            }

        }
        return false;
    }

    //前序遍历
    @Override
    public void traversal(Visitor<k, v> visitor) {
     if(visitor==null)return;
     traversal(root,visitor);
    }
    private void traversal(Node<k,v> node,Visitor<k,v>visitor)
    {
        if(node==null||visitor.stop)
        {
            return;
        }
        traversal(node.left,visitor);
        if (visitor.stop) return;
        visitor.visit(node.key, node.value);
        traversal(node.right,visitor);
    }

    private static class Node<k,v> {
        k key;
        v value;
        boolean color = RED;
        Node<k, v> left;
        Node<k, v> right;
        Node<k, v> parent;

        public Node(k key, v value, Node<k, v> parent) {
            this.key = key;
            this.value = value;
            this.parent = parent;
        }

        public boolean isLeftChild() {
            return parent != null && parent.left ==this;
        }

        public boolean isRightChild() {

            return parent != null && parent.right == this;
        }

        public boolean hasToChildren() {
            return left != null && right != null;
        }

        public Node<k, v> sibling() {
            if (isLeftChild()) {
                return right;
            }
            if (isRightChild()) {
                return left;
            }
            return null;
    }

    public boolean hasTwoChildren(){
            return left!=null&&right!=null;
    }


    }

}

三.用映射实现集合(HashMap实现HashSet)

上面我们提到,映射有两个关键的存储数据key value.映射由 红黑树来实现,在红黑树中如果添加的是相同元素会实现覆盖操作,所以用红黑树实现的映射默认没有重复元素,所以我们不难看出如果我们把红黑树中的value 去掉不就符合集合的所有要求了吗,所以我们完全可以用映射来实现集合。

package bite;

import map.Map;
import map.TreeMap;

import java.util.Objects;

//用映射实现集合
//映射去掉value正好符合集合的要求
public class TreeSet<E> implements Set<E>{
    Map<E, Objects> map=new TreeMap<>();

    @Override
    public int size() {
        return map.size();
    }

    @Override
    public boolean isEmpty() {
        return map.isEmpty();
    }

    @Override
    public void clear() {
        map.clear();
    }

    @Override
    public boolean contains(E element) {
        return map.containsKey(element);
    }

    @Override
    public void add(E element) {
        map.put(element,null);
    }

    @Override
    public void remove(E element) {
        map.remove(element);
    }

    @Override
    public void traversal(Visitor<E> visitor) {
        map.traversal(new Map.Visitor<E, Objects>() {
            @Override
            public boolean visit(E key, Objects value) {
                visitor.visit(key);
                return false;
            }
        });

    }
}

详细代码请见:映射 集合
在这里插入图片描述

  • 12
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@Starry

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值