美文网首页技术栈
算法基础 树专题 二叉树

算法基础 树专题 二叉树

作者: 烟雨乱平生 | 来源:发表于2019-09-25 16:34 被阅读0次

    二叉树

    每个节点最多含有两个子树的树称为二叉树。

    二叉树的性质

    • 在非空二叉树中,第i层的节点数不超过2 i-1i>=1;
    • 深度为h的二叉树最多有2 i -1个节点,最少有h个节点,h>=1;
    • 对于任意一颗二叉树,如果其叶子节点数为N,而度数为2的节点总数为M,则N=M+1;

    其中又分为满二叉树和完全二叉树

    • 满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点。也可以这样理解,除叶子结点外的所有结点均有两个子结点。节点数达到最大值,所有叶子结点必须在同一层上。
    • 完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~(h-1)层) 的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这就是完全二叉树。


      image.png

      完全二叉树是效率很高的数据结构,堆是一种完全二叉树或近似完全二叉树。

    /**
     * 二叉树
     */
    public class BinaryTree<E> {
    
        public BinaryTree(){}
    
        /**
         * 循环构建二叉树
         */
        private Node<E> buildBinaryTree(E[] array) {
            List<Node<E>> nodes = new ArrayList<>(array.length);
            for (E value:array){
                nodes.add(new Node<>(value));
            }
            for (int i = 0; i < array.length/2; i++) {
                //左孩子
                if ((2*i+1)<array.length){
                    nodes.get(i).setLeftChild(nodes.get(2*i+1));
                }
                //右孩子
                if ((2*i+1)<array.length){
                    nodes.get(i).setRightChild(nodes.get(2*i+2));
                }
            }
            return nodes.get(0);
        }
    
        /**
         * 递归构造二叉树
         */
        public Node<E> buildBinaryTree(E[] array,int index){
            if(index>=array.length){
                return null;
            }
            Node<E> node = new Node<>(array[index]);
            if(index*2+1<array.length){
                node.setLeftChild(buildBinaryTree(array,index*2+1));
            }
            if (index*2+2<array.length){
                node.setRightChild(buildBinaryTree(array,index*2+2));
            }
            return node;
        }
    
    
        /**
         * 先序遍历(先根节点->遍历左子树->遍历右子树)
         * [1,2,4,8,9,5,3,6,7]
         */
        public void preOrderTraverse(Node<E> node){
            if (node==null){
                return;
            }
            System.out.print(node.getValue()+",");
            preOrderTraverse(node.getLeftChild());
            preOrderTraverse(node.getRightChild());
        }
    
    
        /**
         * 中序遍历(遍历左子树->根节点->遍历右子树)
         * [8,4,9,2,5,1,6,3,7]
         */
        public void inOrderTraverse(Node<E> node){
            if (node==null){
                return;
            }
            inOrderTraverse(node.getLeftChild());
            System.out.print(node.getValue()+",");
            inOrderTraverse(node.getRightChild());
        }
    
        /**
         * 后序遍历(遍历左子树->遍历右子树->根节点)
         * [7,6,3,5,9,8,4,2,1]
         */
        public void postOrderTraverse(Node<E> node){
            if (node==null){
                return;
            }
            postOrderTraverse(node.getRightChild());
            postOrderTraverse(node.getLeftChild());
            System.out.print(node.getValue()+",");
        }
    
    
        /**
         * 深度优先搜索
         * 简称DFS,其原则是,沿着一条路径一直找到最深的那个节点,
         * 当没有子节点的时候,返回上一级节点,寻找其另外的子节点,
         * 继续向下遍历,没有就向上返回一级,直到所有的节点都被遍历到,
         * 每个节点只能访问一次。
         * [1,2,4,8,9,5,3,6,7]
         * 非递归
         */
        public void depthOrderTraversal(Node<E> root){
            Stack<Node<E>> stack = new Stack<>();
            stack.push(root);
            while(!stack.isEmpty()){
                Node<E> node = stack.pop();
                System.out.print(node.getValue()+",");
                if (node.getRightChild()!=null){
                    stack.push(node.getRightChild());
                }
                if (node.getLeftChild()!=null){
                    stack.push(node.getLeftChild());
                }
            }
        }
    
    
        /**
         * 广度优先搜索
         * 简称BFS;广度优先遍历的原则就是对每一层的节点依次访问,
         * 一层访问结束后,进入下一层,直到最后一个节点,
         * 同样的,每个节点都只访问一次。
         * [1,2,3,4,5,6,7,8,9]
         * 非递归
         */
        public void levelOrderTraversal(Node<E> root){
            Queue<Node<E>> queue = new LinkedBlockingQueue();
            queue.add(root);
            while (!queue.isEmpty()){
                Node<E> node = queue.remove();
                System.out.print(node.getValue()+",");
                if (node.getLeftChild()!=null){
                    queue.add(node.getLeftChild());
                }
                if (node.getRightChild()!=null){
                    queue.add(node.getRightChild());
                }
            }
        }
    
    
        /**
         * 深度优先遍历
         * 递归
         */
        public void dfs(Node<E> root){
            if (root==null){
                return;
            }
            System.out.print(root.getValue()+",");
            dfs(root.leftChild);
            dfs(root.rightChild);
        }
    
    
    
        /**
         * 获取树的深度
         */
        public int getDepth(Node node){
            if (node==null){
                return 0;
            }
            return 1+(getDepth(node.getLeftChild())>getDepth(node.getRightChild())?getDepth(node.getLeftChild()):getDepth(node.getRightChild()));
        }
    
    
        static class Node<E>{
            private E value;
            private Node<E> leftChild;
            private Node<E> rightChild;
    
            public Node(E e){
                value = e;
            }
    
            public E getValue() {
                return value;
            }
    
            public void setValue(E value) {
                this.value = value;
            }
    
            public Node<E> getLeftChild() {
                return leftChild;
            }
    
            public void setLeftChild(Node<E> leftChild) {
                this.leftChild = leftChild;
            }
    
            public Node<E> getRightChild() {
                return rightChild;
            }
    
            public void setRightChild(Node<E> rightChild) {
                this.rightChild = rightChild;
            }
        }
    }
    

    排序二叉树

    二叉查找树(英语:Binary Search Tree),也称为二叉搜索树、有序二叉树(ordered binary tree)或排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:

    • 若任意节点的左子树不空,则左子树上所有节点的值均小于它根节点的值
    • 若任意节点的右子树不空,则右子树上所有节点的值均大于它根节点的值
    • 没有键值相等的节点
    • 对二叉查找树进行中序遍历,即可得到有序的数列

    二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低为 O ( log ⁡ n )

    /**
     * 二叉查找树(排序二叉树)
     */
    public class BinarySearchTree {
    
        Node root;
    
        public Node getRoot(){
            return root;
        }
    
    
        /**
         * 通过数组构造二叉排序树
         */
        public void buildBinarySearchTree(int[] array){
            root = new Node(array[0]);
            for (int i = 1; i < array.length; i++) {
                Node child = new Node(array[i]);
                buildBinarySearchTree(root,child);
            }
        }
    
        /**
         * 向排序二叉树中插入一个节点
         */
        public void buildBinarySearchTree(int value){
            Node node = new Node(value);
            buildBinarySearchTree(node);
        }
    
    
        /**
         * 递归构造二叉排序树
         */
        private void buildBinarySearchTree(Node parent,Node child){
            //右子树
            if (child.getValue()>parent.getValue()){
                if (parent.getRight()==null){
                    parent.setRight(child);
                    return;
                }else{
                    buildBinarySearchTree(parent.getRight(),child);
                }
            }
            //左子树
            if(child.getValue()<parent.getValue()){
                if (parent.getLeft()==null){
                    parent.setLeft(child);
                    return;
                }else{
                    buildBinarySearchTree(parent.getLeft(),child);
                }
            }
        }
    
        /**
         * 非递归构造排序二叉树
         */
        private void buildBinarySearchTree(Node node){
            if (root==null){
                root = node;
                return;
            }
            Node parent = root;
            while (true){
                //右子树
                if (node.getValue()>parent.getValue()){
                    if (parent.getRight()==null){
                        parent.setRight(node);
                        return;
                    }else{
                        parent = parent.getRight();
                        continue;
                    }
                }
                //左子树
                if (node.getValue()<parent.getValue()){
                    if (parent.getLeft()==null){
                        parent.setLeft(node);
                        return;
                    }else{
                        parent = parent.getLeft();
                        continue;
                    }
                }
            }
        }
    
        /**
         * 中序遍历
         */
        public void inOrderTraverse(Node root){
            if (root==null){
                return;
            }
            inOrderTraverse(root.getLeft());
            System.out.print(root.getValue()+",");
            inOrderTraverse(root.getRight());
        }
    
        /**
         * 获取树的深度
         */
        public int getDepth(Node root){
            if (root==null){
                return 0;
            }
            return 1+(getDepth(root.getRight())>getDepth(root.getLeft())?getDepth(root.getRight()):getDepth(root.getLeft()));
        }
    
    
        /**
         * 查找值
         */
        public boolean find(int value){
            Node parent = root;
            while (parent!=null){
                if (value==parent.getValue()){
                    return true;
                }else if (value>parent.getValue()){
                    parent = parent.getRight();
                }else if (value<parent.getValue()){
                    parent = parent.getLeft();
                }
            }
            return false;
        }
    
    
        /**
         * 二叉树节点
         */
        static class Node{
            int value;
            Node left;
            Node right;
    
            public Node(int value){
                this.value = value;
            }
    
            public int getValue() {
                return value;
            }
    
            public void setValue(int value) {
                this.value = value;
            }
    
            public Node getLeft() {
                return left;
            }
    
            public void setLeft(Node left) {
                this.left = left;
            }
    
            public Node getRight() {
                return right;
            }
    
            public void setRight(Node right) {
                this.right = right;
            }
        }
    }
    

    平衡二叉树

    当且仅当任何节点的两棵子树的高度差不大于1的二叉树。其中AVL树是最先发明的自平衡二叉查找树,是最原始典型的平衡二叉树。

    平衡二叉树是基于二叉查找树的改进。由于在某些极端的情况下(如在插入的序列是有序的时),二叉查找树将退化成近似链或链,此时,其操作的时间复杂度将退化成线性的,即O(n)。所以我们通过自平衡操作(即旋转)构建两个子树高度差不超过1的平衡二叉树。

    /**
     * 平衡二叉树
     */
    public class BalancedBinaryTree {
    
        Node root;
    
        public Node getRoot(){
            return root;
        }
    
        public void insert(int value){
            if (null==root){
                root = new Node(value);
            }else {
                insert(root,new Node(value));
            }
        }
    
        /**
         * 插入节点
         */
        private void insert(Node parent,Node child){
            //查找右节点
            if (child.getValue()>parent.getValue()){
                if (parent.getRight()==null){
                    parent.setRight(child);
                }else {
                    insert(parent.getRight(),child);
                    //回溯的时候平衡树
                    balance(parent);
                }
            }
            //查找做节点
            if (child.getValue()<parent.getValue()){
                if (parent.getLeft()==null){
                    parent.setLeft(child);
                }else {
                    insert(parent.getLeft(),child);
                    //回溯的时候平衡树
                    balance(parent);
                }
            }
        }
    
        /**
         * 平衡二叉查找树
         */
        private void balance(Node parent) {
            if (isBalance(parent)){
                return;
            }
            RotationModel model = judgeRotationModel(parent);
            switch (model){
                case LL:
                    rightRotation(parent);
                    break;
                case LR:
                    leftRotation(parent);
                    rightRotation(parent);
                    break;
                case RL:
                    rightRotation(parent);
                    leftRotation(parent);
                    break;
                case RR:
                    rightRotation(parent);
                    break;
                default:
                    break;
            }
        }
    
        /**
         * 左旋转
         * (这里的balance指的是平衡后)
         */
        private void leftRotation(Node balance){
            Node leftChild = balance.getLeft();
            Node rightGrandSon = leftChild.getRight();
            leftChild.setRight(null);
            balance.setLeft(rightGrandSon);
            rightGrandSon.setLeft(leftChild);
        }
    
        /**
         * 右旋转
         * (这里的balance指的是平衡后)
         */
        private void rightRotation(Node balance){
            Node parent = searchParent(balance);
            Node leftChild = balance.getLeft();
            Node rightGrandSon = leftChild.getRight();
            if (parent==null){
                root = leftChild;
            }else {
                //将父节点的孩子替换为非平衡节点的左孩子
                if (parent.getRight()==balance){
                    parent.setRight(leftChild);
                }else {
                    parent.setLeft(leftChild);
                }
            }
            //将原本的非平衡节点设置为右节点
            leftChild.setRight(balance);
            //将右子孙节点设置为原来非平衡节点的左节点
            balance.setLeft(rightGrandSon);
        }
    
        /**
         * 判断节点是否平衡
         */
        private boolean isBalance(Node parent) {
            return getDepth(parent.getRight())-getDepth(parent.getLeft())<2
                    &&getDepth(parent.getRight())-getDepth(parent.getLeft())>-2;
        }
    
        /**
         * 判断二叉树的旋转状态
         */
        private RotationModel judgeRotationModel(Node balance){
            if (getDepth(balance.getRight())>getDepth(balance.getLeft())){
                if (balance.getRight().getRight()==null){
                    return RotationModel.RL;
                }else{
                    return RotationModel.RR;
                }
            }else{
                if (balance.getLeft().getLeft()==null){
                    return RotationModel.LR;
                }else {
                    return RotationModel.LL;
                }
            }
        }
    
        /**
         * 查找节点的父亲节点
         */
        private Node findParent(Node child){
            if (child==root){
                return null;
            }
            return findParent(root,child);
        }
    
        /**
         * 递归查找子节点的父亲节点
         */
        private Node findParent(Node parent,Node child){
            Node target;
            if (parent==null){
                return null;
            }
            if (parent.getLeft()==child||parent.getRight()==child){
                target = parent;
            }else if (child.getValue()>parent.getValue()){
                target = findParent(parent.getRight(),child);
            }else {
                target = findParent(parent.getLeft(),child);
            }
            return target;
        }
    
        /**
         * 迭代查找子节点的父亲节点
         */
        private Node searchParent(Node child){
            if (child==root){
                return null;
            }
            Node parent = root;
            while (parent!=null){
                if (parent.getLeft()==child||parent.getRight()==child){
                    return parent;
                } else if (child.getValue()>parent.getValue()){
                    parent = parent.getRight();
                }else {
                    parent = parent.getLeft();
                }
            }
            return null;
        }
    
        /**
         * 获取节点的深度
         */
        public int getDepth(Node node){
            if (node==null){
                return 0;
            }
            return 1+(getDepth(node.getLeft())>getDepth(node.getRight())
                    ?getDepth(node.getLeft()):getDepth(node.getRight()));
        }
    
        /**
         * 中序遍历
         */
        public void inOrderTraverse(Node root){
            if (root==null){
                return;
            }
            inOrderTraverse(root.getLeft());
            System.out.print(root.getValue()+",");
            inOrderTraverse(root.getRight());
        }
    
        static class Node{
            int value;
            Node left;
            Node right;
    
            public Node(int value){
                this.value = value;
            }
    
            public int getValue() {
                return value;
            }
    
            public void setValue(int value) {
                this.value = value;
            }
    
            public Node getLeft() {
                return left;
            }
    
            public void setLeft(Node left) {
                this.left = left;
            }
    
            public Node getRight() {
                return right;
            }
    
            public void setRight(Node right) {
                this.right = right;
            }
        }
    
        enum RotationModel{
            LL,LR,RR,RL
        }
    }
    

    红黑树

    红黑树也是一种弱平衡或者说自平衡(不是绝对的平衡)的二叉查找树,在平衡二叉树的基础上每个节点又增加了一个颜色的属性,节点的颜色只能是红色或黑色。红黑树的特点是:

    • 根节点必须是黑色
    • 所有叶子都是黑色
    • 每个红色结点的两个子结点一定都是黑色
    • 红黑树中所有的叶子节点后面再接上左右两个空节点,这样可以保持算法的一致性,而且所有的空节点都是黑色。
    • 任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

    AVL树是带有平衡条件的二叉查找树,一般是用平衡因子差值判断是否平衡并通过旋转来实现平衡,左右子树树高相差不超过1,和红黑树相比,AVL树是严格的平衡二叉树,平衡条件必须满足。不管我们是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,因而它的旋转是非常耗时的。

    由于维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常严格整体平衡的红黑树。

    红黑树一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树。由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度低于红黑树。相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,我们就用红黑树。

    红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。所以红黑树的插入效率更高。

    红黑树有两大操作保证平衡:
    - recolor
    - rotation

    image.png

    插入节点的场景


    image.png
    /**
     * 红黑树
     */
    public class RedBlackTree {
    
        Node root;
    
        public Node getRoot(){
            return this.root;
        }
    
        /**
         * 向树中插入节点
         */
        public void insert(int value){
            if (root==null){
                Node node = new Node(NodeColor.BLACK,value);
                root = node;
            }else {
                Node node = new Node(NodeColor.RED,value);
                insert(root,node);
            }
        }
    
        /**
         * 插入节点
         */
        private void insert(Node parent,Node node){
            //右子树
            if (node.getValue()>parent.getValue()){
                if (parent.getRight()==null){
                    parent.setRight(node);
                    selfBalance(node);
                    return;
                }else {
                    insert(parent.getRight(),node);
                }
            }
            //左子树
            if (node.getValue()<parent.getValue()){
                if (parent.getLeft()==null){
                    parent.setLeft(node);
                    selfBalance(node);
                    return;
                }else {
                    insert(parent.getLeft(),node);
                }
            }
        }
    
        /**
         * 红黑树的自平衡
         */
        private void selfBalance(Node node) {
            if (node==null){
                return;
            }
            if (node==root){
                if (node.getColor().equals(NodeColor.RED)){
                    node.setColor(NodeColor.BLACK);
                }
                return;
            }
            Node parent = findParent(node);
            if (parent==root){
                if (parent.getColor().equals(NodeColor.RED)){
                    parent.setColor(NodeColor.BLACK);
                }
                return;
            }
            Node grandParent = findParent(parent);
            Node uncle;
            if (grandParent.getLeft()==parent){
                uncle = grandParent.getRight();
            }else {
                uncle = grandParent.getLeft();
            }
            selfBalance(node,parent,uncle,grandParent);
        }
    
        /**
         * 递归自平衡
         */
        private void selfBalance(Node node, Node parent, Node uncle, Node grandParent) {
            CaseType type = judgeCaseType(node, parent, uncle, grandParent);
            switch (type){
                case PB://父亲是黑色节点直接插入,不用处理
                    break;
                case UR://父亲节点和叔叔节点都是红色
                    parent.setColor(NodeColor.BLACK);
                    uncle.setColor(NodeColor.BLACK);
                    grandParent.setColor(NodeColor.RED);
                    selfBalance(grandParent);
                    break;
                case PR_UB_LL://父亲节点是红色,叔叔节点是黑色或者不存在
                    parent.setColor(NodeColor.BLACK);
                    grandParent.setColor(NodeColor.RED);
                    rightRotation(grandParent);
                    break;
                case PR_UB_LR:
                    leftRotation(parent);
                    selfBalance(parent);
                    break;
                case PR_UB_RR:
                    parent.setColor(NodeColor.BLACK);
                    grandParent.setColor(NodeColor.RED);
                    leftRotation(grandParent);
                    break;
                case PR_UB_RL:
                    rightRotation(parent);
                    selfBalance(parent);
                    break;
            }
        }
    
        /**
         * 对当前节点进行左旋
         */
        private void leftRotation(Node balance){
            Node parent = findParent(balance);
            Node rightChild = balance.getRight();
            Node leftGrandChild = rightChild.getLeft();
            if (parent==root){
                root = rightChild;
            }else {
                if (balance==parent.getLeft()){
                    parent.setLeft(rightChild);
                }else {
                    parent.setRight(rightChild);
                }
            }
            rightChild.setLeft(balance);
            balance.setRight(leftGrandChild);
        }
    
    
        /**
         * 对当前节点进行右旋
         */
        private void rightRotation(Node balance){
            Node parent = findParent(balance);
            Node leftChild = balance.getLeft();
            Node rightGrandChild = leftChild.getRight();
            if (parent==root){
                root = leftChild;
            }else {
                if (balance==parent.getLeft()){
                    parent.setLeft(leftChild);
                }else {
                    parent.setRight(leftChild);
                }
            }
            leftChild.setRight(balance);
            balance.setLeft(rightGrandChild);
        }
    
    
    
        /**
         * 寻找当前节点的父亲节点
         */
        private Node findParent(Node current){
            if (current==root){
                return null;
            }
            Node parent = root;
            while (parent!=null){
                if (parent.getLeft()==current||parent.getRight()==current){
                    return parent;
                }else if (current.getValue()>parent.getValue()){
                    parent = parent.getRight();
                }else {
                    parent = parent.getLeft();
                }
            }
            return null;
        }
    
        /**
         * 判断节点的场景
         */
        public CaseType judgeCaseType(Node node,Node parent,Node uncle,Node grandParent){
            if (parent.getColor().equals(NodeColor.BLACK)){
                return CaseType.PB;
            }else if (uncle!=null&&uncle.getColor().equals(NodeColor.RED)){
                return CaseType.UR;
            }else if (parent==grandParent.getLeft()&&node==parent.getLeft()){
                return CaseType.PR_UB_LL;
            }else if (parent==grandParent.getRight()&&node==parent.getLeft()){
                return CaseType.PR_UB_RL;
            }else if (parent==grandParent.getLeft()&&node==parent.getRight()){
                return CaseType.PR_UB_LR;
            }else if (parent==grandParent.getRight()&&node==parent.getRight()){
                return CaseType.PR_UB_RR;
            }else {
                throw new RuntimeException("未考虑当前场景,存在错误");
            }
        }
    
    
        public void inOrderTraverse(Node root){
            if (root==null){
                return;
            }
            inOrderTraverse(root.getLeft());
            System.out.print(root.getValue()+"["+root.getColor()+"]   ");
            inOrderTraverse(root.getRight());
        }
    
        /**
         * 红黑树节点
         */
        @Setter
        @Getter
        static class Node{
            public Node(NodeColor color,int value){
                this.color = color;
                this.value = value;
            }
            NodeColor color;
            int value;
            Node left;
            Node right;
        }
    
        /**
         * 节点颜色
         */
        enum NodeColor{
            BLACK,RED
        }
    
        /**
         * 场景类型
         * PB——>ParentBlack
         * PR——>ParentRed
         * UB——>UncleBlack
         * UR——>UncleRed
         * */
        enum CaseType{
            PB,//父亲节点是黑色
            UR,//父亲节点是红色,叔叔节点也是红色
    
            /*      A  *
             *    B    *
             *  C      *
             *         */
            PR_UB_LL,//父亲节点是红色,叔叔节点是黑色或者不存在,当前节点和父亲节点是左左结构
    
            /*    A    *
             *  B      *
             *    C    *
             *         */
            PR_UB_LR,//父亲节点是红色,叔叔节点是黑色或者不存在,当前节点和父亲节点是左右结构
    
            /*  A    *
             *    B  *
             *  C    *
             *       */
            PR_UB_RL,//父亲节点是红色,叔叔节点是黑色或者不存在,当前节点和父亲节点是右左结构
    
            /*  A       *
             *    B     *
             *      C   *
             *          */
            PR_UB_RR,//父亲节点是红色,叔叔节点是黑色或者不存在,当前节点和父亲节点是右右结构
            ;
        }
    }
    

    相关文章

      网友评论

        本文标题:算法基础 树专题 二叉树

        本文链接:https://www.haomeiwen.com/subject/gpiiyctx.html