AVL树及其C++实现
来源:互联网 发布:达内java五虎将 编辑:程序博客网 时间:2024/06/06 07:11
介绍
AVL树是根据它的发明者G.M. Adelson-Velsky 和 E.M. Landis命名的。
AVL树是高度平衡的二叉树,它的特点是:AVL树中任何结点的两个子树的高度最大差别为1。
AVL树的查找、插入和删除操作在平均和最坏情况下都是O(logn)。
如果在AVL树中插入或删除结点后,使得高度之差大于1,此时,AVL树的平衡状态就被破坏,它就不是一颗平衡二叉树;为了让它重新维持在一个平衡状态,就需要对其进行旋转处理。学AVL树,重点的地方也就是它的旋转算法。
————————————————————————————————————————————————————————————————————————————
算法实现
树的定义
树的结点
template<class T>class AVLTreeNode{ public: T key; // 关键字(键值) int height; // 高度 AVLTreeNode *left; // 左孩子 AVLTreeNode *right; // 右孩子 AVLTreeNode(T value, AVLTreeNode *l, AVLTreeNode *r) : key(value) ,left(l),right(r) {}};树的类
template<class T>class AVLTree{ private: AVLTreeNode<T> *root; // 根结点 public: // 外部接口 AVLTree(); ~AVLTree(); // 获取树的高度 int height(); // 比较两个值的大小 int max(int a, int b); // 前序遍历 void preOrder(); // 中序遍历 void inOrder(); // 后序遍历 void postOrder(); // (递归实现)查找AVL树中键值为key的结点 AVLTreeNode<T>* search(T key); // (非递归实现)查找AVL树中键值为key的结点 AVLTreeNode<T>* iterativeSearch(T key); // 查找最小结点:返回最小结点的键值 T minimum(); // 查找最大结点:返回最大结点的键值 T maximum(); // 将结点插入到AVL树中 void insert(T key); // 删除结点 void remove(T key); // 销毁AVL树 void destroy(); // 打印AVL树 void print(); private: // 内部接口 // 获取树的高度 int height(AVLTreeNode<T> *tree); // 前序遍历 void preOrder(AVLTreeNode<T> *tree) const; // 中序遍历 void inOrder(AVLTreeNode<T> *tree) const; // 后序遍历 void postOrder(AVLTreeNode<T> *tree) const; // (递归实现)查找AVL树中键值为key的结点 AVLTreeNode<T>* search(AVLTreeNode<T> *x, T key) const; // (非递归实现)查找AVL树中键值为key的结点 AVLTreeNode<T>* iterativeSearch(AVLTreeNode<T> *x, T key) const; // 返回最小结点 AVLTreeNode<T>* minimum(AVLTreeNode<T> *tree); // 返回最大结点 AVLTreeNode<T>* maximum(AVLTreeNode<T> *tree); // 将结点插入到AVL树中 AVLTreeNode<T>* insert(AVLTreeNode<T>* &tree, T key); // 删除结点,并返回被删除的结点 AVLTreeNode<T>* remove(AVLTreeNode<T>* &tree, AVLTreeNode<T> *z); // 销毁AVL树 void destroy(AVLTreeNode<T>* &tree); // 打印AVL树 void print(AVLTreeNode<T> *tree,T key,int direction); // LL:左左对应的情况(左单旋转) AVLTreeNode<T>* leftLeftRotation(AVLTreeNode<T> *k2); // RR:右右对应的情况(右单旋转) AVLTreeNode<T>* rightRightRotation(AVLTreeNode<T> *k1); // LR:左右对应的情况(左双旋转) AVLTreeNode<T>* leftRightRotation(AVLTreeNode<T> *k3); // RL:右左对应的情况(右双旋转) AVLTreeNode<T>* rightLeftRotation(AVLTreeNode<T> *k1); };
树的高度
采用维基百科上的定义:树的高度为最大层次,即空的二叉树的高度是0,非空树的高度等于它的最大层次(根的层次为1,根的子节点为第2层,依次类推)。
template<class T>int AVLTree<T>::height(AVLTreeNode<T> *tree){if(tree!=NULL)return tree->height;return 0;}template<class T>int AVLTree<T>::height(){return height(root);}
旋转
如果在AVL树中进行插入或删除结点后,可能导致AVL树失去平衡。这种失去平衡可以概括为4种姿态:LL(左左)、LR(左右)、RR(右右)、和RL(右左)。
下面给出它们的示意图:
它们各自的定义如下:
1)LL:插入或删除一个结点后,根节点的左子树的左子树还有非空结点,导致根的左子树的高度比根的右子树的高度大2,AVL树因此失去平衡。
例如,在上面LL情况中,由于根节点(8)的左子树(4)的左子树(2)还有非空子节点,而根节点(8)的右子树(12)没有子节点;导致根节点(8)的左子树(4)的高度比根节点(8)的右子树(12)高2。
2)LR:插入或删除一个结点后,根节点的左子树的右子树还有非空结点,导致根的左子树的高度比根的右子树的高度大2,AVL树因此失去平衡。
例如,在上面LR情况中,由于根节点(8)的左子树(4)的右子树(6)还有非空子节点,而根节点(8)的右子树(12)没有子节点;导致根节点(8)的左子树(4)的高度比根节点(8)的右子树(12)高2。
3)RL:插入或删除一个结点后,根节点的右子树的左子树还有非空结点,导致根的右子树的高度比根的左子树的高度大2,AVL树因此失去平衡。
例如,在上面RL情况中,由于根节点(8)的右子树(12)的左子树(10)还有非空子节点,而根节点(8)的左子树(4)没有子节点;导致根节点(8)的右子树(12)的高度比根节点(8)的左子树高2。
4)RR:插入或删除一个结点后,根节点的右子树的右子树还有非空结点,导致根的右子树的高度比根的左子树的高度大2,AVL树因此失去平衡。
例如,在上面RR情况中,由于根节点(8)的右子树(12)的右子树(14)还有非空子节点,而根节点(8)的左子树(4)没有子节点;导致根节点(8)的右子树(12)的高度比根节点(8)的左子树高2。
在AVL树失去平衡之后,可以通过相应的旋转使其恢复平衡,对应的旋转方法如下:
1)LL的旋转
LL失去平衡的情况,可以通过一次旋转让AVL树恢复平衡,如下图:
可以理解为:LL旋转是围绕失去平衡的AVL根节点进行的,也就是节点k2;而且由于是LL情况,就用手抓着左孩子(即k1)使劲摇,使得k1变成根节点,k2变成k1的右子树,k1的右子树变成k2的左子树。
旋转代码:
template<class T>AVLTreeNode<T>* AVLTree<T>::leftLeftRotation(AVLTreeNode<T> *k2){AVLTreeNode<T> *k1;k1 = k2->left;k2->left = k1->right;k1->right = k2;k2->height = max(height(k2->left), height(k2->right)) + 1;k1->height = max(height(k1->left), k2->height) + 1;return k1;}
2)RR的旋转
理解了LL之后,RR就相当容易理解了。RR是LL对称的情况,示意图如下:
旋转代码:
template<class T>AVLTreeNode<T>* AVLTree<T>::rightRightRotation(AVLTreeNode<T> *k1){AVLTreeNode<T> *k2;k2 = k1->right;k1->right = k2->left;k2->left = k1;k1->height = max(height(k1->left), height(k1->right)) + 1;k2->height = max(k1->height, height(k2->right)) + 1;return k2; }
3)LR的旋转
LR失去平衡的情况,需要经过两次旋转才能让AVL树恢复平衡,如下图所示:
第一次旋转是围绕k1进行RR旋转,第二次是围绕k3进行LL旋转。
旋转代码:
template <class T>AVLTreeNode<T>* AVLTree<T>::leftRightRotation(AVLTreeNode<T>* k3){k3->left = rightRightRotation(k3->left);return leftLeftRotation(k3);}
4)RL的旋转
RL是与LR对称的情况,示意图如下:
第一次旋转是围绕k3进行的LL旋转,第二次是围绕k1进行的RR旋转。
旋转代码:
template <class T>AVLTreeNode<T>* AVLTree<T>::rightLeftRotation(AVLTreeNode<T>* k1){k1->right = leftLeftRotation(k1->right);return rightRightRotation(k1);}
插入
template<class T>AVLTreeNode<T>* AVLTree<T>::insert(AVLTreeNode<T>* &tree, T key){if(tree==NULL){// 新建结点 tree = new AVLTreeNode<T>(key, NULL, NULL);if(tree==NULL){cout<<"ERROR: create avltree node failed!"<<endl;return NULL;}}else if(key<tree->key){tree->left = insert(tree->left, key);// 插入结点后,若AVL树失去平衡,则进行相应的调节if(height(tree->left)-height(tree->right)==2){if(key<tree->left->key)tree = leftLeftRotation(tree);elsetree = leftRightRotaion(tree);} }else{tree->right = insert(tree->right, key);// 插入结点后,若AVL树失去平衡,则进行相应的调节if(height(tree->right)-height(tree->left)==2){if(key>tree->right->key)tree = rightRightRotation(tree);elsetree = rightLeftRotaion(tree);} }tree->height = max(height(tree->left), height(tree->right)) + 1;return tree;}template<class T>void AVLTree<T>::insert(T key){insert(root,key);}
删除
template<class T>AVLTreeNode<T>* AVLTree<T>::remove(AVLTreeNode<T>* &tree, AVLTreeNode<T> *z){// 根为空 或者 没有要删除的结点if(tree==NULL || z==NULL)return NULL;if(z->key<tree->key){tree->left = remove(tree->left, z);// 删除结点后,若AVL树失去平衡,则进行相应调节if(height(tree->right)-height(tree->left)==2){AVLTreeNode<T> *r = tree->right;if(height(r->left)>height(r->right))tree = rightLeftRotation(tree);elsetree = rightRightRotation(tree);} } else if(z->key>tree->key){tree->right = remove(tree->right, z);if(height(tree->left)-height(tree->right)==2){AVLTreeNode<T> *r = tree->left;if(height(r->left)<height(r->right))tree = leftRightRotation(tree);elsetree = leftLeftRotation(tree);}}else// tree是对应要删除的结点 {// tree的左右孩子都非空 if((tree->left!=NULL)&&(tree->right!=NULL)){if(height(tree->left)>height(tree->right)){// 如果tree的左子树比右子树高; // 则(01)找出tree的左子树中的最大节点 // (02)将该最大节点的值赋值给tree。 // (03)删除该最大节点。 // 这类似于用"tree的左子树中最大节点"做"tree"的替身; // 采用这种方式的好处是:删除"tree的左子树中最大节点"之后,AVL树仍然是平衡的。 AVLTreeNode<T> *max = maximum(tree->left); tree->key = max->key; tree->left = remove(tree->left, max);}else{// 如果tree的左子树不比右子树高(即它们相等,或右子树比左子树高1) // 则(01)找出tree的右子树中的最小节点 // (02)将该最小节点的值赋值给tree。 // (03)删除该最小节点。 // 这类似于用"tree的右子树中最小节点"做"tree"的替身; // 采用这种方式的好处是:删除"tree的右子树中最小节点"之后,AVL树仍然是平衡的。 AVLTreeNode<T> *min = minimum(tree->right); tree->key = min->key;tree->right = remove(tree->right, min); }}else{AVLTreeNode<T> *tmp = tree;tree = (tree->left!=NULL) ? tree->left : tree->right;delete tmp;}}return tree;}template<class T>void AVLTree<T>::remove(T key){ AVLTreeNode<T> *z; if((z=search(root,key))!=NULL) root = remove(root,z);}
关于AVL树的前序遍历、中序遍历、后序遍历、最大值、最小值、查找、打印、销毁等接口与二叉查找树基本一样,这些已经介绍过了就不再单独介绍了。
完整实现
头文件:AVLTree.h
#ifndef AVL_TREE#define AVL_TREE#include<iomanip>#include<iostream>using namespace std;template<class T>class AVLTreeNode{ public: T key; // 关键字(键值) int height; // 高度 AVLTreeNode *left; // 左孩子 AVLTreeNode *right; // 右孩子 AVLTreeNode(T value, AVLTreeNode *l, AVLTreeNode *r) : key(value) ,left(l),right(r) {}};template<class T>class AVLTree{ private: AVLTreeNode<T> *root; // 根结点 public: // 外部接口 AVLTree(); ~AVLTree(); // 获取树的高度 int height(); // 比较两个值的大小 int max(int a, int b); // 前序遍历 void preOrder(); // 中序遍历 void inOrder(); // 后序遍历 void postOrder(); // (递归实现)查找AVL树中键值为key的结点 AVLTreeNode<T>* search(T key); // (非递归实现)查找AVL树中键值为key的结点 AVLTreeNode<T>* iterativeSearch(T key); // 查找最小结点:返回最小结点的键值 T minimum(); // 查找最大结点:返回最大结点的键值 T maximum(); // 将结点插入到AVL树中 void insert(T key); // 删除结点 void remove(T key); // 销毁AVL树 void destroy(); // 打印AVL树 void print(); private: // 内部接口 // 获取树的高度 int height(AVLTreeNode<T> *tree); // 前序遍历 void preOrder(AVLTreeNode<T> *tree) const; // 中序遍历 void inOrder(AVLTreeNode<T> *tree) const; // 后序遍历 void postOrder(AVLTreeNode<T> *tree) const; // (递归实现)查找AVL树中键值为key的结点 AVLTreeNode<T>* search(AVLTreeNode<T> *x, T key) const; // (非递归实现)查找AVL树中键值为key的结点 AVLTreeNode<T>* iterativeSearch(AVLTreeNode<T> *x, T key) const; // 返回最小结点 AVLTreeNode<T>* minimum(AVLTreeNode<T> *tree); // 返回最大结点 AVLTreeNode<T>* maximum(AVLTreeNode<T> *tree); // 将结点插入到AVL树中 AVLTreeNode<T>* insert(AVLTreeNode<T>* &tree, T key); // 删除结点,并返回被删除的结点 AVLTreeNode<T>* remove(AVLTreeNode<T>* &tree, AVLTreeNode<T> *z); // 销毁AVL树 void destroy(AVLTreeNode<T>* &tree); // 打印AVL树 void print(AVLTreeNode<T> *tree,T key,int direction); // LL:左左对应的情况(左单旋转) AVLTreeNode<T>* leftLeftRotation(AVLTreeNode<T> *k2); // RR:右右对应的情况(右单旋转) AVLTreeNode<T>* rightRightRotation(AVLTreeNode<T> *k1); // LR:左右对应的情况(左双旋转) AVLTreeNode<T>* leftRightRotation(AVLTreeNode<T> *k3); // RL:右左对应的情况(右双旋转) AVLTreeNode<T>* rightLeftRotation(AVLTreeNode<T> *k1); };template<class T>AVLTree<T>::AVLTree(): root(NULL){}template<class T>AVLTree<T>::~AVLTree(){ destroy(root);}template<class T>int AVLTree<T>::height(AVLTreeNode<T> *tree){ if(tree!=NULL) return tree->height; return 0;}template<class T>int AVLTree<T>::height(){ return height(root);}template<class T>int AVLTree<T>::max(int a, int b){ return a>b ? a : b;}template<class T>AVLTreeNode<T>* AVLTree<T>::leftLeftRotation(AVLTreeNode<T> *k2){ AVLTreeNode<T> *k1; k1 = k2->left; k2->left = k1->right; k1->right = k2; k2->height = max(height(k2->left), height(k2->right)) + 1; k1->height = max(height(k1->left), k2->height) + 1; return k1;}template<class T>AVLTreeNode<T>* AVLTree<T>::rightRightRotation(AVLTreeNode<T> *k1){ AVLTreeNode<T> *k2; k2 = k1->right; k1->right = k2->left; k2->left = k1; k1->height = max(height(k1->left), height(k1->right)) + 1; k2->height = max(k1->height, height(k2->right)) + 1; return k2; }template <class T>AVLTreeNode<T>* AVLTree<T>::leftRightRotation(AVLTreeNode<T>* k3){ k3->left = rightRightRotation(k3->left); return leftLeftRotation(k3);}template <class T>AVLTreeNode<T>* AVLTree<T>::rightLeftRotation(AVLTreeNode<T>* k1){ k1->right = leftLeftRotation(k1->right); return rightRightRotation(k1);}template<class T>AVLTreeNode<T>* AVLTree<T>::insert(AVLTreeNode<T>* &tree, T key){ if(tree==NULL) { // 新建结点 tree = new AVLTreeNode<T>(key, NULL, NULL); if(tree==NULL) { cout<<"ERROR: create avltree node failed!"<<endl; return NULL; } } else if(key<tree->key) { tree->left = insert(tree->left, key); // 插入结点后,若AVL树失去平衡,则进行相应的调节 if(height(tree->left)-height(tree->right)==2) { if(key<tree->left->key) tree = leftLeftRotation(tree); else tree = leftRightRotation(tree); } } else { tree->right = insert(tree->right, key); // 插入结点后,若AVL树失去平衡,则进行相应的调节 if(height(tree->right)-height(tree->left)==2) { if(key>tree->right->key) tree = rightRightRotation(tree); else tree = rightLeftRotation(tree); } } tree->height = max(height(tree->left), height(tree->right)) + 1; return tree;}template<class T>void AVLTree<T>::insert(T key){ insert(root,key);}template<class T>AVLTreeNode<T>* AVLTree<T>::remove(AVLTreeNode<T>* &tree, AVLTreeNode<T> *z){ // 根为空 或者 没有要删除的结点 if(tree==NULL || z==NULL) return NULL; if(z->key<tree->key) { tree->left = remove(tree->left, z); // 删除结点后,若AVL树失去平衡,则进行相应调节 if(height(tree->right)-height(tree->left)==2) { AVLTreeNode<T> *r = tree->right; if(height(r->left)>height(r->right)) tree = rightLeftRotation(tree); else tree = rightRightRotation(tree); } } else if(z->key>tree->key) { tree->right = remove(tree->right, z); if(height(tree->left)-height(tree->right)==2) { AVLTreeNode<T> *r = tree->left; if(height(r->left)<height(r->right)) tree = leftRightRotation(tree); else tree = leftLeftRotation(tree); } } else // tree是对应要删除的结点 { // tree的左右孩子都非空 if((tree->left!=NULL)&&(tree->right!=NULL)) { if(height(tree->left)>height(tree->right)) { // 如果tree的左子树比右子树高; // 则(01)找出tree的左子树中的最大节点 // (02)将该最大节点的值赋值给tree。 // (03)删除该最大节点。 // 这类似于用"tree的左子树中最大节点"做"tree"的替身; // 采用这种方式的好处是:删除"tree的左子树中最大节点"之后,AVL树仍然是平衡的。 AVLTreeNode<T> *max = maximum(tree->left); tree->key = max->key; tree->left = remove(tree->left, max); } else { // 如果tree的左子树不比右子树高(即它们相等,或右子树比左子树高1) // 则(01)找出tree的右子树中的最小节点 // (02)将该最小节点的值赋值给tree。 // (03)删除该最小节点。 // 这类似于用"tree的右子树中最小节点"做"tree"的替身; // 采用这种方式的好处是:删除"tree的右子树中最小节点"之后,AVL树仍然是平衡的。 AVLTreeNode<T> *min = minimum(tree->right); tree->key = min->key; tree->right = remove(tree->right, min); } } else { AVLTreeNode<T> *tmp = tree; tree = (tree->left!=NULL) ? tree->left : tree->right; delete tmp; } } return tree;}template<class T>void AVLTree<T>::remove(T key){ AVLTreeNode<T> *z; if((z=search(root,key))!=NULL) root = remove(root,z);}template<class T>void AVLTree<T>::preOrder(AVLTreeNode<T> *tree) const{ if(tree!=NULL) { cout<<tree->key<<" "; preOrder(tree->left); preOrder(tree->right); }}template<class T>void AVLTree<T>::preOrder(){ preOrder(root);}template<class T>void AVLTree<T>::inOrder(AVLTreeNode<T> *tree) const{ if(tree!=NULL) { inOrder(tree->left); cout<<tree->key<<" "; inOrder(tree->right); }}template<class T>void AVLTree<T>::inOrder(){ inOrder(root);}template<class T>void AVLTree<T>::postOrder(AVLTreeNode<T> *tree) const{ if(tree!=NULL) { postOrder(tree->left); postOrder(tree->right); cout<<tree->key<<" "; }}template<class T>void AVLTree<T>::postOrder(){ postOrder(root);}template<class T>AVLTreeNode<T>* AVLTree<T>::search(AVLTreeNode<T> *x, T key) const{ if(x==NULL || x->key==key) return x; if(key<x->key) return search(x->left, key); else return search(x->right, key);}template<class T>AVLTreeNode<T>* AVLTree<T>::search(T key){ return search(root, key);}template<class T>AVLTreeNode<T>* AVLTree<T>::iterativeSearch(AVLTreeNode<T> *x, T key) const{ while(x!=NULL && x->key!=key) { if(key<x->key) x = x->left; else x = x->right; } return x;}template<class T>AVLTreeNode<T>* AVLTree<T>::iterativeSearch(T key){ return iterativeSearch(root, key);}template<class T>AVLTreeNode<T>* AVLTree<T>::minimum(AVLTreeNode<T> *tree){ if(tree==NULL) return NULL; while(tree->left!=NULL) tree = tree->left; return tree;}template<class T>T AVLTree<T>::minimum(){ AVLTreeNode<T> *p = minimum(root); if(p!=NULL) return p->key; return (T)NULL;}template<class T>AVLTreeNode<T>* AVLTree<T>::maximum(AVLTreeNode<T> *tree){ if(tree==NULL) return NULL; while(tree->right!=NULL) tree = tree->right; return tree;}template<class T>T AVLTree<T>::maximum(){ AVLTreeNode<T> *p = maximum(root); if(p!=NULL) return p->key; return (T)NULL;}template<class T>void AVLTree<T>::destroy(AVLTreeNode<T> *&tree){ if(tree==NULL) return ; if(tree->left!=NULL) destroy(tree->left); if(tree->right!=NULL) destroy(tree->right); delete tree;}template<class T>void AVLTree<T>::destroy(){ destroy(root);}template<class T>void AVLTree<T>::print(AVLTreeNode<T> *tree, T key, int direction){ if(tree!=NULL) { if(direction==0) cout<<setw(2)<<tree->key<<"is root"<<endl; else cout<<setw(2)<<tree->key<<"is"<<setw(2)<<key<<"'s"<<setw(12)<<(direction==1 ? "right child" : "left child")<<endl; print(tree->left, tree->key, -1); print(tree->right, tree->key, 1); }}template<class T>void AVLTree<T>::print(){ if(root!=NULL) print(root, root->key, 0);}#endif
测试文件:AVLTreeTest.cpp
#include<iostream>#include "AVLTree.h"using namespace std;static int arr[] = {3,2,1,4,5,6,7,16,15,14,13,12,11,10,8,9};int main(){int i,len;AVLTree<int> *tree = new AVLTree<int>();cout<<"依次添加:";len = sizeof(arr)/sizeof(arr[0]);for(i=0;i<len;++i){cout<<arr[i]<<" ";tree->insert(arr[i]);}cout<<"\n前序遍历:";tree->preOrder();cout<<"\n中序遍历:";tree->inOrder();cout<<"\n后序遍历:";tree->postOrder();cout<<"\n高度: "<<tree->height()<<endl; cout<<"最小值: "<<tree->minimum()<<endl; cout<<"最大值: "<<tree->maximum()<<endl; cout<<"树的详细信息: "<<endl; tree->print(); i = 8; cout<<"\n删除根节点:"<<i; tree->remove(i); cout<<"\n高度: "<<tree->height()<<endl; cout<<"中序遍历:"; tree->inOrder(); cout<<"\n树的详细信息: "<<endl; tree->print(); // 销毁二叉树tree->destroy();return 0; }
测试程序示意图:
(1)添加3,2:
(2)添加1:
(3)添加4:
(4)添加5:
(5)... ....
(6)添加完所有数据后,得到的AVL树如下:
(7)删除节点8:
程序运行结果如下:
参考资料:
感谢作者如果天空不死 :AVL树
- AVL树及其实现
- AVL树及其C++实现
- AVL树及其C++实现
- AVL树C实现
- AVL树C实现
- AVL树C实现
- AVL树-C实现
- avl树 C实现
- AVL树原理思想及其实现
- AVL树C语言实现
- AVL树 C语言实现
- C 数据结构 AVL树实现
- AVL树(c实现)
- AVL树C语言实现
- avl树的c实现
- AVL树--C语言实现
- AVL树--C语言实现
- c语言实现AVL树
- JQuery回顾--常用选择器
- 多重背包
- unix网络编程第一个程序-获取时间(含借鉴)
- 等了两天了还没解决
- asp关于新消息弹出题在线等解决立马结贴
- AVL树及其C++实现
- PKU 2891 Strange Way to Express Integers(线性同余方程组)
- 高手请进在线等待
- 1015.Reversible Primes (20)
- CentOS下安装node
- spring 自动装配
- fir.im Weekly
- PHP浮点数的精度
- javascript jquery保留两位小数