目录

1、底层结构

2、AVL树的概念

3、AVL树节点的定义

4、基本框架

5、AVL树的插入

6、AVL树的旋转

左单旋

右单旋

左右双旋

右左双旋

7、AVL树的验证

8、AVL树的查找

9、AVL树的删除(了解)

10、AVL树的性能

11、源码链接


1、底层结构

前面对map、multimap、set、multiset进行了简单的介绍,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。


2、AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  1. 它的左右子树都是AVL树
  2. 任何一颗左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
  • 0代表左右高度相等
  • 1代表右子树高1
  • -1代表左子树高1

下面给出一幅图来判断是否为AVL树:

如果一棵二叉搜索树是高度平衡的(相对平衡),它就是AVL树。如果它有n个结点,其高度可保持在
O(logN),搜索时间复杂度O(logN)。


3、AVL树节点的定义

这里我们实现的AVL树为KV模型,自然节点的模板参数有两个,并且节点定义为三叉连结构(左孩子,右孩子,父亲),在二叉链表的基础上加了一个指向父结点的指针域,使得即便于查找孩子结点,又便于查找父结点。接着还需要创建一个变量_bf作为平衡因子(右子树 - 左子树的高度差)。最后写一个构造函数初始化变量即可。

//节点类
template<class K, class V>
struct AVLTreeNode
{//存储的键值对pair<K, V> _kv;//三叉连结构AVLTreeNode<K, V>* _left;//左孩子AVLTreeNode<K, V>* _right;//右孩子AVLTreeNode<K, V>* _parent;//父亲//平衡因子_bfint _bf;//右子树 - 左子树的高度差//构造函数AVLTreeNode(const pair<K, V>& kv):_kv(kv), _left(nullptr), _right(nullptr), _bf(0){}
};

4、基本框架

此部分内容为AVL树的类,主要作用是来完成后续的插入旋转删除……操作:

//AVL树的类
template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public://……
private:Node* _root;
};

5、AVL树的插入

插入主要分为这几大步骤:

  • 1、一开始为空树,直接new新节点
  • 2、一开始非空树,寻找插入的合适位置
  • 3、找到插入的合适位置后,进行父亲与孩子的双向链接
  • 4、更新新插入的节点祖先的平衡因子
  • 5、针对不合规的平衡因子进行旋转调整

接下来对其进行逐个分析:

  • 1、一开始为空树,直接new新节点:

因为树为空的,所以直接new一个新插入的节点,将其作为根_root即可,接着更新平衡因子_bf为0,最后返回true。

  • 2、一开始非空树,寻找插入的合适位置:

这里和二叉搜索树的寻找合适的插入位置的思想一样,都要遵循以下几步:

  1. 插入的值 > 节点的值,更新到右子树查找
  2. 插入的值 < 节点的值,更新到左子树查找
  3. 插入的值 = 节点的值,数据冗余插入失败,返回false

当循环结束的时候,就说明已经找到插入的合适位置,即可进行下一步链接。

  • 3、找到插入的合适位置后,进行父亲与孩子的双向链接:

注意这里节点的构成为三叉链,因此最后链接后端孩子和父亲是双向链接,具体操作如下:

  1. 插入的值 > 父亲的值,把插入的值链接在父亲的右边
  2. 插入的值 < 父亲的值,把插入的值链接在父亲的左边
  3. 因为是三叉连,插入后记得双向链接(孩子链接父亲)

走到这,说明节点已经插入完毕,但是接下来就要更新平衡因子了

  • 4、更新新插入的节点祖先的平衡因子:

当我们插入新节点后,子树的高度可能会发生变化,针对这一变化,我们给出以下要求:

  1. 子树的高度变了,就要继续往上更新
  2. 子树的高度不变,则更新完成
  3. 子树违反平衡规则(平衡因子的绝对值 >= 2),则停止更新,需要旋转子树进行调整

具体的更新规则如下:

  1. 新增结点在parent的右边,parent的平衡因子++
  2. 新增结点在parent的左边,parent的平衡因子 --

每更新完一个节点的平衡因子后,都要进行如下判断:

  1. 如果parent的平衡因子等于-1或者1(说明原先是0,左右登高,插入节点后使左子树或右子树增高了)。表明还需要继续往上更新平衡因子。
  2. 如果parent的平衡因子等于0(说明原先是1或-1,一高一低,插入节点后填上了矮的那一方)表明无需更新平衡因子了。
  3. 如果parent的平衡因子等于-2或者2(说明原先是1或-1,一高一低,插入节点后填上了高的那一方),表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。

图示如下:

  • 5、针对不合规的平衡因子进行旋转调整:

当父亲parent的平衡因子为2或-2时,就要进行旋转调整了,而又要分为以下4类进行旋转:

  1. 当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。
  2. 当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋
  3. 当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。
  4. 当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。

代码如下:

//Insert插入
bool Insert(const pair<K, V>& kv)
{//1、一开始为空树,直接new新节点if (_root == nullptr){//如果_root一开始为空树,直接new一个kv的节点,更新_root和_bf_root = new Node(kv);_root->_bf = 0;return true;}//2、寻找插入的合适位置Node* cur = _root;//记录插入的位置Node* parent = nullptr;//保存parent为cur的父亲while (cur){if (cur->_kv.first < kv.first){//插入的值 > 节点的值parent = cur;cur = cur->_right;//更新到右子树查找}else if (cur->_kv.first > kv.first){//插入的值 < 节点的值parent = cur;cur = cur->_left;//更新到左子树查找}else{//插入的值 = 节点的值,数据冗余插入失败,返回falsereturn false;}}//3、找到了插入的位置,进行父亲与插入节点的链接cur = new Node(kv);if (parent->_kv.first < kv.first){//插入的值 > 父亲的值,链接在父亲的右边parent->_right = cur;}else{//插入的值 < 父亲的值,链接在父亲的左边parent->_left = cur;}//因为是三叉连,插入后记得双向链接(孩子链接父亲)cur->_parent = parent;//4、更新新插入节点的祖先的平衡因子while (parent)//最远要更新到根{if (cur == parent->_right){parent->_bf++;//新增结点在parent的右边,parent的平衡因子++}else{parent->_bf--;//新增结点在parent的左边,parent的平衡因子 --}//判断是否继续更新?if (parent->_bf == 0)// 1 or -1 -> 0 填上了矮的那一方{//1 or -1 -》 0 填上了矮的那一方,此时正好,无需更新break;}else if (parent->_bf == 1 || parent->_bf == -1){//0 -》 1或-1  此时说明插入节点导致一边变高了,继续更新祖先cur = cur->_parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){//1 or -1 -》2或-2 插入节点导致本来高的一边又变更高了//此时子树不平衡,需要进行旋转if (parent->_bf == 2 && cur->_bf == 1){RotateL(parent);//右边高,左单旋}else if (parent->_bf == -2 && cur->_bf == -1){RotateR(parent);//左边高,右单旋}else if (parent->_bf == -2 && cur->_bf == 1){RotateLR(parent);//左右双旋}else if (parent->_bf == 2 && cur->_bf == -1){RotateRL(parent);//右左双旋}break;}else{//插入之前AVL树就存在不平衡树,|平衡因子| >= 2的节点//实际上根据前面的判断不可能走到这一步,不过这里其实是为了检测先前的插入是否存在问题assert(false);}}return true;
}

6、AVL树的旋转

AVL树的旋转分为4种:

  1. 左单旋
  2. 右单旋
  3. 左右双旋
  4. 右左双旋

AVL树的旋转要遵循下面两个原则:

  • 1、保持搜索树的规则
  • 2、子树变平衡

左单旋

  • 条件:新节点插入较高右子树的右侧

图示:

鉴于左单旋的情况非常多,这里我们画一张抽象图来演示:

这里的长方形条(a、b、c)表示的是子树,h为子树的高度,而30和60为实打实的节点。上述左单旋操作主要是完成了四件事:

  1. 让subRL变成parent的右子树,更新subRL的父亲为parent
  2. 让subR变成根节点
  3. 让parent变成subR的左子树,更新parent的父亲为subR
  4. 更新平衡因子

注意:

  1. parent可能为整棵树的一个子树,则需要链接parent的父亲和subR。
  2. subRL可能为空,但是更新subRL的父亲为parent是建立在subRL不为空的前提下完成的。

解释为何上述左单旋的可行性:

  • 首先,根据底层二叉搜索树的结构:b节点的值肯定是在30~60之间的,b去做30的右子树没有任何问题,且这里把60挪到根部,随即把30作为60的右子树,这样整体的变化就像是一个左旋一样,而且也满足二叉搜索树的性质且均平衡。

代码如下:

void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;Node* ppNode = parent->_parent;//提前保持parent的父亲//1、建立parent和subRL之间的关系parent->_right = subRL;if (subRL)//防止subRL为空{subRL->_parent = parent;}//2、建立subR和parent之间的关系subR->_left = parent;parent->_parent = subR;//3、建立ppNode和subR之间的关系if (parent == _root){_root = subR;_root->_parent = nullptr;}else{if (parent == ppNode->_left){ppNode->_left = subR;}else{ppNode->_right = subR;}subR->_parent = ppNode;//三叉链双向链接关系}//4、更新平衡因子subR->_bf = parent->_bf = 0;
}

右单旋

  • 条件:新节点插入较高左子树的左侧

图示:

同样这里的长方形条(a、b、c)表示的是子树,h为子树的高度,而30和60为实打实的节点。上述左单旋操作主要是完成了四件事:

  1. 让subLR变成parent的左子树,更新subLR的父亲为parent
  2. 让subL变成根节点
  3. 让parent变成subL的右子树,更新parent的父亲为subL
  4. 更新平衡因子

注意:

  1. parent可能为整棵树的一个子树,则需要链接parent的父亲和subL。
  2. subLR可能为空,但是更新subLR的父亲为parent是建立在subLR不为空的前提下完成的。

代码如下:

//2、右单旋
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;Node* ppNode = parent->_parent;//1、建立parent和subLR之间的关系parent->_left = subLR;if (subLR){subLR->_parent = parent;}//2、建立subL和parent之间的关系subL->_right = parent;parent->_parent = subL;//3、建立ppNode和subL的关系if (parent == _root){_root = subL;_root->_parent = nullptr;}else{if (parent == ppNode->_left){ppNode->_left = subL;}else{ppNode->_right = subL;}subL->_parent = ppNode;//三叉链双向关系}//4、更新平衡因子subL->_bf = parent->_bf = 0;
}

左右双旋

  • 条件:新节点插入较高左子树的右侧

接下来图示解析左右双旋的具体解法。

  • 1、插入新节点:

此类模型既不满足左单旋的条件也不满足右单旋的条件,但是我们可以将其组合起来,即左右双旋(先左单旋,再右单旋)的办法重新建立平衡。接下来执行下一步左单旋:

  • 2、以节点30(subL)为旋转点左单旋:

此时再观察这幅图,这部就是一个妥妥的右单旋模型吗,把60的左子树看成一个整体,此时新插入的节点即插入较高左子树的左侧,刚好符合右单旋的性质,接下来即可进行右单旋:

  • 3、以节点90(parent)为旋转点进行右单旋:

此时左右双旋已经完成,最后一步为更新平衡因子,但是更新平衡因子又分如下三类:

  • 1、当subLR原始平衡因子是-1时,左右双旋后parent、subL、subLR的平衡因子分别更新为1、0、0。

  • 2、当subLR原始平衡因子是1时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、-1、0。

  • 3、当subLR原始平衡因子是0时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0。

这里可以看出唯有subLR平衡因子为0的情况下在进行左右旋转后,三个节点的平衡因子都要更新为0。

  • 代码如下:
//3、左右双旋
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;//提前记录subLR的平衡因子//1、以subL为根传入左单旋RotateL(subL);//2、以parent为根传入右单旋RotateR(parent);//3、重新更新平衡因子if (bf == 0){parent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 1){parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else if (bf == -1){parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else{assert(false);//此时说明旋转前就有问题,检查}
}

右左双旋

  • 条件:新节点插入较高右子树的左侧

接下来图示解析左右双旋的具体解法。

  • 1、插入新节点:

注意这里的新节点插在了较高右子树的左侧,不能用上文的单旋转以及左右旋转,相反而应使用右左旋转(先右单旋,再左单旋)来解决,接下来执行下一步右单旋:

  • 2、以节点90(subR)为旋转点右单旋:

此时再观察这幅图,这部就是一个妥妥的左单旋模型吗,把60的右子树看成一个整体,此时新插入的节点即插入较高右子树的右侧,刚好符合左单旋的性质,接下来即可进行左单旋:

  • 3、以节点30(parent)为旋转点左单旋:

此时左右双旋已经完成,最后一步为更新平衡因子,但是更新平衡因子又分如下三类:

  • 1、当subRL原始平衡因子是-1时,右左双旋后parent、subR、subRL的平衡因子分别更新为0、1、0。

  • 2、当subRL原始平衡因子是1时,右左双旋后parent、subR、subRL的平衡因子分别更新为-1、0、0。

  • 3、当subRL原始平衡因子是0时,右左双旋后parent、subR、subRL的平衡因子分别更新为0、0、0。

这里可以看出唯有subRL平衡因子为0的情况下在进行左右旋转后,三个节点的平衡因子都要更新为0。

  • 代码如下:
//4、右左双旋
void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;//提前记录subLR的平衡因子//1、以subL为根传入左单旋RotateR(subR);//2、以parent为根传入右单旋RotateL(parent);//3、重新更新平衡因子if (bf == 0){parent->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}else if (bf == 1){parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else if (bf == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else{assert(false);//此时说明旋转前就有问题,检查}
}

7、AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,就是看它是否为二叉搜索树,以及是否为一颗平衡树。接下来分别讨论:

  • 1、验证其为二叉搜索树:

这里我们只需要进行中序遍历看看结果是否可得到一个有序的序列,如果可以则证明是二叉搜索树,而中序遍历的实现非常简单,先前的二叉搜索树的实现已然完成过,这里直接给出代码:

//中序遍历的子树
void _InOrder(Node* root)
{if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);
}
//中序遍历
void InOrder()
{_InOrder(_root);cout << endl;
}

检测是否为二叉搜索树的代码实现后,接下来开始验证是否为平衡树。

  • 2、验证其为平衡树:

规则如下:(递归的思想)

  1. 空树也是平衡树,一开始就要判断
  2. 封装一个专门计算高度的函数(递归计算高度)后续用来计算高度差(平衡因子diff)
  3. 如过diff不等于root的平衡因子(root->_bf),或root平衡因子的绝对值超过1,则一定不是AVL树
  4. 继续递归到子树&&右树,直至结束

代码如下:

//验证一棵树是否为平衡树
bool IsBalanceTree()
{return _IsBalanceTree(_root);
}
//判读是否平衡的子树
bool _IsBalanceTree(Node* root)
{//空树也是AVL树if (nullptr == root)return true;//计算root节点的平衡因子diff:即root左右子树的高度差int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int diff = rightHeight - leftHeight;//如果计算出的平衡因子与root的平衡因子不相等,或root平衡因子的绝对值超过1,则一定不是AVL树if ((abs(diff) > 1)){cout << root->_kv.first << "节点平衡因子异常" << endl;return false;}if (diff != root->_bf){cout << root->_kv.first << "节点平衡因子与root的平衡因子不等,不符合实际" << endl;return false;}//继续递归检测,直到结束return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
//求高度的子树
int _Height(Node* root)
{if (root == nullptr)return 0;int lh = _Height(root->_left);int rh = _Height(root->_right);return lh > rh ? lh + 1 : rh + 1;
}

综合上述两大步骤的操作即可对一棵树充分的验证是否为AVL树。


8、AVL树的查找

Find查找函数的思路很简单,定义cur指针从根部开始按如下规则遍历:

  1. 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
  2. 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
  3. 若key值等于当前结点的值,则查找成功,返回true。
  4. 若遍历一圈cur走到nullptr了说明没有此结点,返回false
//Find查找
bool Find(const K& key)
{Node* cur = _root;while (cur){if (cur->_key < key){cur = cur->_right;//若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。}else if (cur->_key > key){cur = cur->_left;//若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。}else{return true;//若key值等于当前结点的值,则查找成功,返回true。}}return false;//遍历一圈没找到返回false
}

9、AVL树的删除(了解)

因为AVL树也是二叉搜索树,主要是三个大思路:

  1. 按二叉搜索树的规则删除
  2. 更新平衡因子
  3. 出现不平衡,需要旋转调整

只不过与搜索二叉树的删除不同的是,删除节点后的平衡因子需要不断更新,最差情况下一直要调整到根节点的位置。具体这里就不再实现了,因为着实有点复杂。不过《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版这两本书上是有详细的讲解的哈。


10、AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即O(logN)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。


11、源码链接

链接直达:AVL树的模拟实现完整版

【 C++ 】AVL树相关推荐

  1. AVL树、splay树(伸展树)和红黑树比较

    AVL树.splay树(伸展树)和红黑树比较 一.AVL树: 优点:查找.插入和删除,最坏复杂度均为O(logN).实现操作简单 如过是随机插入或者删除,其理论上可以得到O(logN)的复杂度,但是实 ...

  2. 算法基础知识科普:8大搜索算法之AVL树(上)

    前段时间介绍了二叉搜索树(BST),我们知道这种搜索结构存在的弊端是对输入序列存在强依赖,若输入序列基本有序,则BST近似退化为链表.这样就会大大降低搜索的效率.AVL树以及Red-Black树就是为 ...

  3. 【漫画】以后在有面试官问你AVL树,你就把这篇文章扔给他。

    S 推荐阅读 对于另一半,你真正的爱过吗? 背景 西天取经的路上,一样上演着编程的乐趣..... 1.若它的左子树不为空,则左子树上所有的节点值都小于它的根节点值. 2.若它的右子树不为空,则右子树上 ...

  4. 【从蛋壳到满天飞】JS 数据结构解析和算法实现-AVL树(一)

    前言 [从蛋壳到满天飞]JS 数据结构解析和算法实现,全部文章大概的内容如下: Arrays(数组).Stacks(栈).Queues(队列).LinkedList(链表).Recursion(递归思 ...

  5. 数据结构与算法——AVL树类的C++实现

    关于AVL树的简单介绍能够參考: 数据结构与算法--AVL树简单介绍 关于二叉搜索树(也称为二叉查找树)能够參考:数据结构与算法--二叉查找树类的C++实现 AVL-tree是一个"加上了额 ...

  6. 纸上谈兵: AVL树

    作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 二叉搜索树的深度与搜索效率 我们在树, 二叉树, 二叉搜索树中提到,一个有n个节点 ...

  7. AVL树入门(转载)

    原文链接:http://lib.csdn.net/article/datastructure/9204           作者:u011469062 前言:本文不适合 给一组数据15分钟就能实现AV ...

  8. PAT1123 Is It a Complete AVL Tree(AVL树完全二叉树)

    题意: 给出一系列数,要求组成AVL树,最后层序输出,并且判断是否为一个完全二叉树 要点: 这题就是一个AVL树的插入和判断完全二叉树,之前分别都有出现过,AVL树的建立需要记忆. #include& ...

  9. Linux内核之于红黑树and AVL树

    为什么Linux早先使用AVL树而后来倾向于红黑树?        实际上这是由红黑树的实用主义特质导致的结果,本短文依然是形而上的观点.红黑树可以直接由2-3树导出,我们可以不再提红黑树,而只提2- ...

  10. 算法(6) —— AVL树

    AVL树二叉查找树的一种,所以其操作和二叉查找树的很多操作是相同的. 1. 1 #ifndef AVLTREE_H 2 #define AVLTREE_H 3 4 struct AvlNode; 5 ...

最新文章

  1. 第十六 django进一步了解
  2. 哪个更快:while(1)或while(2)?
  3. wxpython使用folium_wxPython实现文本框基础组件
  4. 【Windows Phone】Metro设计语言
  5. MTK 移植泰文输入法
  6. keepalived配置高可用集群
  7. JStorm—实时流式计算框架入门介绍
  8. pat1045. Favorite Color Stripe (30)
  9. 十万大学生都已成为猿粉,你还在等什么?
  10. xp计算机管理窗口,XP系统设备管理器的打开技巧
  11. 趣学算法 陈小玉 书中所有问题的实现代码
  12. Testbed单元测试
  13. ewebeditor php漏洞,ewebeditor for php任意文件上传漏洞
  14. 地球物理中的有限单元法-第二类边界条件-三角剖分-线性插值 matlab编程实现
  15. NOIP 2015 蒟蒻做题记录
  16. 深入理解Java7.pdf
  17. MATLAB 数据频数统计
  18. 中小企业如何选择进销存软件?
  19. Python——打开文件
  20. 2018.12.30【NOIP提高组】模拟A组 JZOJ 5353 村通网

热门文章

  1. Android DataBinding RecyclerView AAPT: error: attribute adapter (aka......) not found.
  2. zend guard loader php ts,安装Zend Guard Loader说明
  3. hadoop大数据工程师、数据开发工程师、数据仓库工程师 面试题目分享
  4. 【青龙面板】返利好省
  5. word2vec训练中文词向量
  6. wkhtmltopdf乱码解决方案
  7. 奋斗吧,程序员——第二十四章 想佳人、妆楼凝望,误几回、天际识归舟
  8. 计算机科学与技术考研双非,985弱势“好考”专业与双非王牌专业大汇总!考研报考必备!...
  9. 豆瓣电影Top250信息爬取并保存到excel文件中
  10. click和touchmove vue_移动端touch事件影响click事件以及在touchmove添加preventDefault导致页面无法滚动的解决方法...