美文网首页
数据结构基础学习之(内排序)

数据结构基础学习之(内排序)

作者: JiaJianHuang | 来源:发表于2019-04-09 10:44 被阅读0次

    学习知识

    • 排序基本概念
    • 插入排序的实现方法及性能分析
    • 交换排序的实现方法及性能分析
    • 选择排序的实现方法及性能分析
    • 归并排序的实现方法及性能分析
    • 基数排序的实现方法及性能分析
    • 内部排序比较

    一、 内排序

    1. 概念

    排序分类

    1. 内部排序
    • 插入排序
    • 交换排序
    • 选择排序
    • 归并排序
    2. 外部排序
    3. 稳定排序: 若相同关键字间的前后位置关系在排序前与排序后保持一致,则称为稳定排序; 反之为不稳地排序

    2. 直接插入排序(插入排序,稳定排序方法)

    步骤: 假设待排序的记录存放在数组r[0...n-1]中

    1. 将r[i] 暂存在临时变量temp中
    2. 将temp 与r[j] (j=i-1,i-2,...,0)依次比较,若temp < r[j], 则将r[j]后移,直到temp>=r[j]为止
    3. 将temp插入第(j+1)的位置上
    4. 令i=1,2,..,n重复步骤2,3

    示意图

    insertion sort.png

    代码实现:

     /**
         * 没有设置哨兵的插入排序
         *
         * @param elements
         */
        public void sortNoGuard(int[] elements) {
            int i, j;
            for (i = 1; i < elements.length; i++) {
                //1. 将r[i]存放在临时变量temp中
                int temp = elements[i];
                //2. 将r[j] 与temp比较,当temp <=r[j], 将r[j]往后移
                for (j = i - 1; j >= 0 && temp < elements[j]; j--) {
                    elements[j + 1] = elements[j];
                }
                //3. 将temp插入j+1的位置
                elements[j + 1] = temp;
            }
        }
    
        /**
         * 插入算法改进
         */
        public void sortWithGuard(int[] elements) {
            int i, j;
            int len = elements.length;
            for (i = 1; i < len; i++) {
                //elements[0]充当哨兵,避免边界判断
                elements[0] = elements[i];
    
                for (j = i - 1;  elements[0] < elements[j]; j--) {
                    elements[j + 1] = elements[j];
                }
                //3. 将elements[i]插入j+1的位置
                elements[j + 1] = elements[0];
    
            }
        }
    

    算法分析

    1. 空间复杂度为: O(1)
    2. 时间复杂度:
    • 最好情况: 待排序序列为有序状态,则时间复杂度为:O(n)
    • 最坏和平均情况:
      比较次数: \sum_{i=1}^{n-1}i = \frac12 n(n-1) ; 移动次数为:\sum_{i=1}^{n-1}(i+2) = \frac12 n(n-1)+2n
      每次移动次数(i+2):多出的两次移动次数是临时变量(如temp)保存待插入元素,最后又将temp移动到相应位置上
    • 所以总的平均比较与移动次数:
      \sum_{i=1}^{n-1} \frac i2 = \frac 14 n(n-1)约等于\frac {n^2}4
    • 因此,插入排序的时间复杂度为O(n^2)

    3. 希尔排序(插入排序,不稳定排序)

    步骤:

    1. 选择一个增量序列{d0,d1,d2,d3,..., dk-1}
    2. 根据当前的增量di将n条记录分成di个子表,每个子表中记录的下标相隔为di
    3. 对各个子表中的记录进行直接插入排序
    4. 令i=1,1,..., k-1, 重复步骤2~4

    示意图

    shellsort.png

    代码实现

    /**
         * 希尔排序
         * <p>
         * 时间复杂度: O(nlog2n)
         *
         * @param elements 待排序的序列
         * @param d        增量集合
         */
        public static void sort(int[] elements, int[] d) {
            int i, j;
            for (int k = 0; k < d.length; k++) {
    
                //对每个子表中的记录进行直接插入排序
                int dk = d[k];
                for (i = 0; i < elements.length; i++) {
                    //存临时变量
                    int temp = elements[i];
                    //移动
                    for (j = i - dk; j >= 0 && temp < elements[j]; j -= dk) {
                        elements[j + dk] = elements[j];
                    }
                    elements[j + dk] = temp;
                }
            }
        }
    

    复杂度分析

    1. 空间复杂度:O(1)
    2. 时间复杂度:O(nlog2n)
    4. 冒泡排序(交换排序,稳定)

    步骤:

    1. 置初值 i=1
    2. 在无序序列{r[0],r[1],..., r[n-i]}中,从头至尾依次比较相邻的两个记录r[j]与r[j+1] (0<=j<=n-i-1), 若r[j]>r[j+1]则置换位置
    3. i=i+1
    4. 重复步骤2,3

    示意图

    7.3bubblesort.png

    代码实现

    /**
         * 冒泡排序
         * <p>
         * 空间复杂度:O(1)
         * 时间复杂度: O(n^2)
         *
         * @param elements
         */
        public void sort(int[] elements) {
    
            //标识序列是否发生交换
            boolean swap_flag = true;
            for (int i = 1, len = elements.length; i < len && swap_flag; i++) {
                //标识没有发生交换
                swap_flag = false;
                for (int j = 0; j < len - i; j++) {
                    if (elements[j] > elements[j + 1]) {
                        int temp = elements[j];
                        elements[j] = elements[j + 1];
                        elements[j + 1] = temp;
                        //标识交换
                        swap_flag = true;
                    }
                }
            }
        }
    

    复杂度

    1. 空间复杂度:O(1)
    2. 时间复杂度:
    • 最好情况:在已有序情况下,一趟比较下,没有发生交换,只需比较了n-1次,时间复杂度:O(n)
    • 最坏情况:在逆序情况下,总共要比较次数为(n-1)次,第 i 趟排序中,比较次数n-i, 移动次数为3*(n-i); 总比较次数为:\sum_{i=1}^{n-1} (n-i) = \frac12 n(n-1) ; 总移动次数:\sum_{i=1}^{n-1} 3(n-i) = \frac32 n(n-1)
    • 平均时间复杂度:O(n^2)
    5. 快速排序(交换,不稳定排序)

    步骤:(partition 函数)

    1. 设置两个变量i,j, 初始值为low和hight,分别代表待排序序列的起始位置和终止位置
    2. 将第i个记录暂存在变量pivot中,即pivot=r[i]
    3. 从下标j的位置开始由后向前依次搜索,当找到一个比pivot的关键字值小的元素时,则将该元素 替换第 i 个 元素,然后i=i+1
    4. 从下标i的位置开始由前向后依次搜索,当找到一个比pivot的关键字值大的记录时,则将该该元素 替换第 j 个元素,然后j=j+1
    5. 重复步骤:3, 4 步,直到i==j为止
    6. r[i] = pivot

    示意图

    7.4quicksort.png

    代码实现

     /**
         * @param i
         * @param j
         * @return
         */
        public static int partition(int[] elements, int i, int j) {
            //把第i个元素作为支点
            int pivot = elements[i];
            ////从表的两端向中间扫描
            while (i < j) {
                //找到elements[i] <= elements[j]的下标
                while (i < j && pivot <= elements[j]) {
                    j--;
                }
    
                //当elements[i] <= elements[j], 交换位置
                if (i < j) {
                    elements[i] = elements[j];
                    i++;
                }
    
                // elements[i] > elements[j]的下标
                while (i < j && pivot > elements[j]) {
                    i++;
                }
    
                //当elements[i] > elements[j], 交换位置
                if (i < j) {
                    elements[j] = elements[i];
                    j--;
                }
            }
            //支点记录到位
            elements[i] = pivot;
            //返回支点下标
            return i;
        }
    
        /**
         * 快速排序
         *
         * @param elements 待排序列
         * @param low
         * @param hight
         */
        public static void qSort(int[] elements, int low, int hight) {
            if (low < hight) {
                int pivotloc = partition(elements, low, hight);
                // 低子表排序
                qSort(elements, low, pivotloc - 1);
                //高子表排序
                qSort(elements, pivotloc + 1, hight);
            }
        }
    

    算法分析

    • 空间复杂度:平均O(log2n),最坏情况:O(n)
    • 时间复杂度:
      • 最好情况: O(nlog2n)
      • 最坏情况:O(n^2)
      • 平均: O(nlog2n)

    6. 直接选择排序(Straight Selection sort不稳定排序)

    算法步骤:

    1. 置i值为0
    2. 当i<n-1时,重复下列步骤:
    • 在无序子序列{r[i+1],..., r[n-1]}中选出一个关键字最小的记录r[min]
    • 若min != i , 则交换r[i]和r[min]的位置,否则不交换
    • i++

    示意图

    7.5StraightSelectSort.png

    代码实现

    public static void straightSelectionSort(int[] elements) {
            int i, j, minIndex;
            //n-1趟排序
            for (i = 0; i < elements.length - 1; i++) {
                //定义最小值索引为 i
                minIndex = i;
                //从i+1开始,找最小值
                for (j = i + 1; j < elements.length; j++) {
                    if (elements[j] < elements[minIndex]) {
                        //记录最小值索引
                        minIndex = j;
                    }
                }
    
                //如果最小值索引不等于i, 则与i交换位置
                if (minIndex != i) {
                    int temp = elements[i];
                    elements[i] = elements[minIndex];
                    elements[minIndex] = temp;
                }
                
            }
        }
    

    算法分析

    • 空间复杂度: O(1)
    • 时间复杂度:
      • 外循环执行:n-1次; 内循环执行:n-1-i次
      • 总比较次数: \frac 12 n(n-1)
      • 时间复杂度:O(n^2)
    7. 树形选择排序(选择,稳定排序) :解决直接选择排序关键字多次比较问题

    基本思想

    1. 首先对n各记录进行两两比较,比较的结果时把关键字值较小者作为优胜者上升为父结点,得到n/2个优胜者,然后再对这n/2个优胜者进行两两比较,重复直到选出一个最小关键字为止。这个过程可以用完全二叉树表示,称为胜者树

    算法步骤:

    1. 初始化,令待排序结点的个数为n, 完全二叉树的叶子数leafSize(必须为2的幂), 胜者树所有结点TreeSize = 2* leafSize - 1; 叶结点开始下标loadIndex = leafSize - 1;
    2. 将待排序列复制到胜者树的叶结点中
    3. 构造胜利树,即叶结点两两比较,得到n/2个关键字值小的结点,保存为叶结点的父结点中,再对这些结点进行两两比较,直到得到最小的关键字保存再根结点为止。
    4. 调整胜者树: 先把胜者树的根结点保存到原数组中,再把具有根结点值所对叶子结点的值修改为“最大值”,然后从该结点开始,和其左(右)兄弟结点进行比较,修改从该叶结点到根的路径上各结点的值,直到根结点。
    5. 重复步骤4,直到得到n个结点为止。

    示意图

    7.6 TreeSelectionSort.png

    代码实现

       enum ActiveFlag {
            ACTIVE,//参选
            UNACTIVE;//不参选
        }
    
        /**
         * 胜者树节点
         */
        class TreeNode {
            //数据
            public int data;
            //该节点的索引
            public int index;
            //标识
            public ActiveFlag active;
        }
    
    
        public void treeSelectionSort(int[] elements) {
            //胜者树结点数组
            TreeNode[] tree;
            // 叶子节点数
            int leafSize = 1;
    
            //得到胜者树的叶子结点数, 该结点数必须为2的幂,才能构造成满二叉树
            while (leafSize < elements.length) {
                leafSize = leafSize * 2;
            }
    
    
            //胜者树的所有结点数
            //二叉树的第i层结点数最多: 2^i
            //二叉树的所有结点数最多: 2^k-1 = 2*2^i-1 (k为深度)
            int treeSize = 2 * leafSize - 1;
    
            // 叶子结点数存放的起始位置
            int loadIndex = leafSize - 1;
            //创建胜者树数组
            tree = new TreeNode[treeSize];
            // 待排序序列下标
            int j = 0;
            //把待排序结点复制到胜者树的叶子结点中
            for (int i = loadIndex; i < treeSize; i++) {
                tree[i] = new TreeNode();
                tree[i].index = i;
    
                if (j < elements.length) {
                    tree[i].active = ActiveFlag.ACTIVE;
                    tree[i].data = elements[j++];
                } else {
                    tree[i].active = ActiveFlag.UNACTIVE;
                }
            }
    
            //先进行一次比较查找关键字值最小的结点
            int i = loadIndex;
            while (i > 0) {
                j = i;
                //处理各对比赛对手
                while (j < 2 * i) {
                    //完全二叉树的性质:
                    //若i=0,则结点为根结点,没有双亲,若i>0,则它的双亲结点编号为 (i-1)/2
                    if (tree[j + 1].active == ActiveFlag.UNACTIVE || (tree[j].data <= tree[j + 1].data)) {
                        //左孩子胜者,晋级(成为父节点)
                        tree[(j - 1) / 2] = tree[j];
                    } else {
                        //右孩子胜者,晋级(成为父节点)
                        tree[(j - 1) / 2] = tree[j + 1];
                    }
                    //下一对比赛选手
                    j += 2;
                }
                i = (i - 1) / 2;
            }
    
            //继续找出剩余最小
            for (i = 0; i < elements.length - 1; i++) {
                //将胜者树的根(最小者)存入数组
                elements[i] = tree[0].data;
                //该结点选手落选,不再比赛
                tree[tree[0].index].active = ActiveFlag.UNACTIVE;
                //调整胜者树,再次两两比赛,筛选出最小值
                updateTree(tree, tree[0].index);
            }
            elements[elements.length - 1] = tree[0].data;
        }
    
    
        private void updateTree(TreeNode[] tree, int index) {
            // 比赛对手的索引
            int j;
    
            if (index % 2 == 0) {
                //index 为偶数,对手为左结点
                tree[(index - 1) / 2] = tree[index - 1];
            } else {
                //index 为奇数,对手为右结点
                tree[(index - 1) / 2] = tree[index + 1];
            }
            //最小记录输出,其对手上升到父结点
            index = (index - 1) / 2;
            while (index > 0) {
                if (index % 2 == 0) {
                    //index 为偶数,对手为左结点
                    j = index - 1;
                } else {
                    //index 为奇数,对手为右结点
                    j = index + 1;
                }
    
                //比赛对手中有一个为空
                if (tree[index].active == ActiveFlag.UNACTIVE || tree[j].active == ActiveFlag.UNACTIVE) {
                    if (tree[index].active == ActiveFlag.ACTIVE) {
                        //i晋级,
                        tree[(index - 1) / 2] = tree[index];
                    } else {
                        //落选
                        tree[(index - 1) / 2] = tree[j];
                    }
                } else {
                    if (tree[index].data <= tree[j].data) {
                        tree[(index - 1) / 2] = tree[index];
                    } else {
                        tree[(index - 1) / 2] = tree[j];
                    }
                }
                index = (index - 1) / 2;
            }
        }
    
    
    

    算法分析

    • 空间复杂度:当对于含有n个记录的顺序表,当n=2^k, 需要使用2n-1个结点来存放胜者树;当2^k-1<n<2^k时, 需要使用2*2^k-1个结点
    • 时间复杂度:树形选择排序过程可以用满二叉树来表示,对于有n个结点的满二叉树,其高度为k = [log2n+1], 除第一次筛选最小关键值的记录,需要比较n-1次,调整胜利树的比较次数为O(log2n),总的关键字比较次数为O(nlog2n)
    8. 堆排序(不稳定排序):解决树形排序占空间大问题

    基本思想:

    1. 首先将这n条记录按关键字值的大小建成堆(初始堆),将堆顶元素r[0]与r[n-1]交换(或输出);然后,将剩下的r[0]...r[n-2]序调整成堆,再r[0]与r[n-2]交换,又将剩下的r[0]...r[n-3]序列调整成堆,如此反复,便可以得到一个按关键字值有序的记录序列

    示意图

    7.8两个堆实例及存储结构.png

    算法步骤:

    • 筛选法调整堆步骤:
    1. 置初始值 i = low,j = 2*i+1,temp = r[i];
    2. 当j < hight时,重复以下操作
      1. 若j<hight-1; r[j] > r[j+1], 则 j ++
      2. 若temp > r[j] , 则r[i] = r[j], j = 2*i+1; 否则j= higth +1
    3. r[i] = temp;
    7.9 调整堆.png
    • 堆排序的步骤:
    1. 将待排序序列{r[0],r[1],r[2],..., r[n-1]}构造成一棵完全二叉树
    2. 将下标(n/2 - 1) 的记录作为开始调整的子树的根结点
    3. 找出此结点的两个孩子结点中的关键字值较小的,将其与父结点比较,若父结点的关键字值较大,则交换,然后以交换的子结点作为新的父结点,重复此步骤,直到没有子结点为止
    4. 以步骤3 中原来父结点所在位置往前推一个位置,作为新的调整的子树的根结点。继续重复步骤3,直到调整到树根。此时初始堆已形成
    5. 堆建成后,将树根与二叉树的最后一个结点交换后,再将最后一个结点输出(即输出原本的树根),然后比较根结点的两个子结点,若左子结点的关键字值较小,则调整左子树;反之,调整右子树,使它再成为堆。
    6. 重复步骤5,直到二叉树仅剩下一个结点为止
    7.10构造堆.png 7.11堆排序过程.png

    代码实现:

    /**
         * 筛选法调整堆
         * <p>
         * 以low为根结点的子树调整为小顶堆
         */
        public static void sift(int low, int hight, int[] elements) {
            //定义根结点的索引
            int i = low;
            int j = 2 * i + 1;
            //暂时值
            int temp = elements[i];
            //
            while (j < hight) {
    
                //比较左右子孩子的大小,找出最小的结点
                if (j < hight - 1 && elements[j] > elements[j + 1]) {
                    j++;
                }
                //父结点与最小子结点比较,如果父结点大于子节点,则交换位置
                if (temp > elements[j]) {
                    elements[i] = elements[j];
                    //以j为父结点的子树,再次调整堆
                    i = j;
                    j = 2 * i + 1;
                } else {
                    //退出循环
                    j = hight + 1;
                }
            }
            elements[i] = temp;
        }
    
        /**
         * 堆排序
         *
         * @param elements
         */
        public static void headSort(int[] elements) {
            //创建堆
            int len = elements.length;
            //2. 以下标(n/2-1) 作为开始调整子树的根结点,进行筛选法调整堆
            for (int i = (len / 2 - 1); i >= 0; i--) {
                sift(i, len, elements);
            }
            for (int i = len - 1; i > 0; i--) {
                //将根结点与二叉树最后一个结点交换
                int temp = elements[0];
                elements[0] = elements[i];
                elements[i] = temp;
                //使用筛选法调整堆法,调整为大顶堆
                sift(0, i, elements);
            }
        }
    

    算法分析:

    • 空间复杂度: 堆排序需要一个记录的辅助存储空间,空间复杂度为:O(1)
    • 时间复杂度:假设堆排序过程产生的二叉树高度为k, 则k = log2n + 1. 从树的根结点到叶子结点的筛选,关键字的比较次数至多2(k-1)次, 交换记录至多为k次。
      所以,在建好堆后,排序过程中的筛选次数不超过2 * [log2(n-1) + log2(n-2)+...+log2(2)]<2nlog2n. 在最坏的情况下,堆排序算法的时间复杂度O(nlog2n)
    9. 归并排序(Merging Sort, 稳定排序)

    基本思想

    1. 将待排序记录r[0] 到r[n-1] 看成是一个含有n个长度为1的有序子表
    2. 把这些子表依次进行两两归并,得到n/2个有序的子表;
    3. 然后,再把这n/2个有序的子表进行两两归并,
    4. 如此重复,直到最后得到一个长度为n的有序表为止

    示意图

    [图片上传中...(7.12二路归并排序示例.png-978f33-1554628574110-0)]

    代码实现

       /**
         * 归并两个相邻的有序表r[h]-r[m] 和r[m+1]-r[t] 归并为r[h]-r[t]
         *
         * @param r     待排序序列
         * @param order 归并后序列
         * @param h     第一个有序表第一个元素的下标
         * @param m     第一个有序表第最后元素的下标
         * @param t     第二个有序表第最后元素的下标
         */
        public void merge(int[] r, int[] order, int h, int m, int t) {
            int i = h, j = m + 1, k = h;
    
            //将r中两个相邻子序列归并到order中
            while (i <= m && j <= t) {
                //较小值复制到order中
                if (r[i] <= r[j]) {
                    order[k++] = r[i++];
                } else {
                    order[k++] = r[j++];
                }
            }
    
            //将前一个子序列剩余元素复制到order中
            while (i <= m) {
                order[k++] = r[i++];
            }
    
            //将后一个子序列剩余元素复制到order中
            while (j <= t) {
                order[k++] = r[j++];
            }
        }
    
        /**
         * 归并排序
         *
         * @param r     待排序序列
         * @param order 归并结果序列
         * @param s     待归并有序子序列的长度
         * @param n     待排序序列长度
         */
        public void mergepass(int[] r, int[] order, int s, int n) {
            //定义每对待合并表的第一个元素的下标,初始值为0
            int p = 0;
            //两两归并长度均为s的有序表
            //p: 有序表的开始元素下标,p+s-1: 有序表的结束元素下标
            while (p + 2 * s - 1 <= n - 1) {
                merge(r, order, p, p + s - 1, p + 2 * s - 1);
                p = p + 2 * s;
            }
            //归并最后两个长度不等的有序表
            if (p + s - 1 < n - 1) {
                merge(r, order, p, p + s - 1, n - 1);
            } else {
                //将剩余有序表复制到order中
                for (int i = p; i <= n - 1; i++) {
                    order[i] = r[i];
                }
            }
        }
    
    
        /**
         * 二路归并算法
         */
        public void mergeSort(int[] elements) {
            int s = 1;
            int len = elements.length;
            int[] temp = new int[len];
            while (s < len) {
    
                mergepass(elements, temp, s, len);
                s = s * 2;
                mergepass(temp, elements, s, len);
                s = s * 2;
            }
        }
    

    算法分析

    • 空间复制度:二路归并排序需要一个与待排序记录序列等长的辅助数组来存放排序过程,所以空间复杂度为O(n)
    • 时间复杂度:归并趟数:log2n, 每趟移动的次数为n , 所以二路归并排序的时间复杂度为O(nlog2n)
    10. 基数排序(Radix Sort,稳定排序)

    概念:

    1. 基数排序是一种借助于多关键字进行排序

    示意图(图来自 skywang12345

    7.13 基数排序.png

    基本思想

    1. 将一个序列中的逻辑关键字看成由d个关键字复合而成,并采用最低位优先方法对该序列进行多个关键字排序,即从最低关键字开始,将整个序列中的元素“分配”到rd个队列中,再依次“收集”成一个新的序列,如此重复进行d次,即完成排序过程

    代码实现:

    /**
         * 获取序列最大值
         */
        private static int getMax(int[] array) {
            int max = array[0];
            for (int i = 1; i < array.length; i++)
                if (max < array[i])
                    max = array[i];
            return max;
    
        }
    
        /**
         * 按对数组按照"某个位数"进行排序
         *
         * @param arr 数组
         * @param n   数组长度
         * @param exp 排序的位数
         */
        private static void countSort(int[] arr, int n, int exp) {
            int output[] = new int[n]; //输出的数组
            int i;
            int count[] = new int[10];
    
            // 保存出现的次数
            //如:179,589 按个位排序(9) ;在count数组中的最后一个count[9] = 2;
            for (i = 0; i < n; i++)
                count[(arr[i] / exp) % 10]++;
    
            // 更改count[i]。目的是让更改后的count[i]的值,是该数据在output[]中的位置。
            for (i = 1; i < 10; i++)
                count[i] += count[i - 1];
    
            // 将数据存储到临时数组output[]中
            for (i = n - 1; i >= 0; i--) {
                output[count[(arr[i] / exp) % 10] - 1] = arr[i];
                count[(arr[i] / exp) % 10]--;
            }
    
            // 将排序好的数据赋值给a[]
            for (i = 0; i < n; i++)
                arr[i] = output[i];
        }
    
        public static void radixSort(int arr[]) {
            //获取最大值
            int m = getMax(arr);
            //长度
            int n = arr.length;
    
            // 从个位开始,对数组a按"指数"进行排序
            for (int exp = 1; m / exp > 0; exp *= 10)
                countSort(arr, n, exp);
        }
    

    11. 总结

    1. 各种内部排序算法的性能比较

    2. 选择排序算法建议:

    • 简单算法: 冒泡、直接选择,直接插入
    • 改进算法:希尔、堆、归并、快速
    • 若待排序记录的个数n越小,采用简单排序算法合适,反之,采用改进排序算法合适
    • 若记录序列初始状态基本有序,则应选择直接插入排序、冒泡排序
    1. 各个排序比较


      7.1各种内部排序算法的性能比较.png

    相关文章

      网友评论

          本文标题:数据结构基础学习之(内排序)

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