zoukankan      html  css  js  c++  java
  • acm算法模板(5)

    STL 中 sort 函数用法简介 

    做 ACM 题的时候,排序是一种经常要用到的操作。如果每次都自己写个冒泡之类的 O(n^2) 排序,不但程序容易超时,而且浪费宝贵的比赛时间,还很有可能写错。 STL 里面有个 sort 函数,可以直接对数组排序,复杂度为 n*log2(n) 。

    使用这个函数,需要包含头文件#include <algorithm>。

    这个函数可以传两个参数或三个参数。第一个参数是要排序的区间首地址,第二个参数是区间尾地址的下一地址。也就是说,排序的区间是 [a,b) 。简单来说,有一个数组 int a[100] ,要对从 a[0] 到 a[99] 的元素进行排序,只要写 sort(a,a+100) 就行了,默认的排序方式是升序。

    拿我出的“ AC 的策略”这题来说,需要对数组 t 的第 0 到 len-1 的元素排序,就写 sort(t,t+len); 

    对向量 v 排序也差不多, sort(v.begin(),v.end()); 

    排序的数据类型不局限于整数,只要是定义了小于运算的类型都可以,比如字符串类 string 。

    如果是没有定义小于运算的数据类型,或者想改变排序的顺序,就要用到第三参数——比较函数。比较函数是一个自己定义的函数,返回值是 bool 型,它规定了什么样的关系才是“小于”。想把刚才的整数数组按降序排列,可以先定义一个比较函数 cmp 

    bool cmp(int a,int b) 

    return a>b; 

    排序的时候就写 sort(a,a+100,cmp); 

    假设自己定义了一个结构体 node 

    struct node{ 

    int a; 

    int b; 

    double c; 

    有一个 node 类型的数组 node arr[100] ,想对它进行排序:先按 a 值升序排列,如果 a 值相同,再按 b 值降序排列,如果 b 还相同,就按 c 降序排列。就可以写这样一个比较函数:

    以下是代码片段:

    bool cmp(node x,node y) 

    if(x.a!=y.a) return x.a 

    if(x.b!=y.b) return x.b>y.b; 

    return return x.c>y.c; 

    排序时写 sort(arr,a+100,cmp); 

    最后看一个完整的实例,初赛时的一道题目“文件名排序 ”。

    以下是代码片段:

    #include<iostream> 

    #include<algorithm> 

    #include<string> 

    using namespace std; 

    // 定义一个结构体来表示文件, a 代表文件名, b 代表文件类型(要么 "File" 要么 "Dir" )

    struct node{ 

    string a,b; 

    }; 

    //ASCII 码中,所有大写字母排在所有小写字母前面, 'A'<'Z'<'a'<'z' 

    // 而这题要求忽略大小写,所以不能直接用字符串的比较。自定义了一个 lt 函数,就是 less than 的意思

    // 先把两个字符串全部转化为小写,再比较大小(字典序)

    bool lt(string x,string y) 

    int i; 

    for(i=0;i<x.length();i++) 

    if(x[i]>='A'&&x[i]<='Z') 

    x[i]='a'+(x[i]-'A'); 

    for(i=0;i<y.length();i++) 

    if(y[i]>='A'&&y[i]<='Z') 

    y[i]='a'+(y[i]-'A'); 

    return x<y; 

    // 自定义的比较函数,先按 b 值升序排列(也就是 "Dir" 排在 "File" 前面)

    // 如果 b 值相同,再按 a 升序排列,用的是刚才定义的 lt 函数

    bool comp(node x,node y) 

    if(x.b!=y.b)return x.b<y.b; 

    return lt(x.a,y.a); 

    int main() 

    node arr[10001]; 

    int size=0; 

    while(cin>>arr[size].a>>arr[size].b) 

    size++; 

    sort(arr,arr+size,comp); 

    for(int i=0;i<size;i++) 

    cout<<arr[i].a<<" "<<arr[i].b<<endl; 

    return 0; 

    七种 qsort 排序方法 

    < 本文中排序都是采用的从小到大排序 > 

    一、对 int 类型数组排序 

    int num[100]; 

    Sample: 

    int cmp ( const void *a , const void *b ) 

    return *(int *)a - *(int *)b; 

    qsort(num,100,sizeof(num[0]),cmp); 

    二、对 char 类型数组排序(同 int 类型) 

    char word[100]; 

    Sample: 

    int cmp( const void *a , const void *b ) 

    return *(char *)a - *(char *)b; 

    qsort(word,100,sizeof(word[0]),cmp); 

    三、对 double 类型数组排序(特别要注意) 

    double in[100]; 

    int cmp( const void *a , const void *b ) 

    return *(double *)a > *(double *)b ? 1 : -1; 

    qsort(in,100,sizeof(in[0]),cmp) ; 

    四、对结构体一级排序 

    struct In 

    double data; 

    int other; 

    }s[100] 

    // 按照 data 的值从小到大将结构体排序 , 关于结构体内的排序关键数据 data 的类型可以很多种,参考上面的例子写 

    int cmp( const void *a ,const void *B) 

    return (*(In *)a)->data > (*(In *)B)->data ? 1 : -1; 

    qsort(s,100,sizeof(s[0]),cmp); 

    五、对结构体二级排序 

    struct In 

    int x; 

    int y; 

    }s[100]; 

    // 按照 x 从小到大排序,当 x 相等时按照 y 从大到小排序 

    int cmp( const void *a , const void *b ) 

    struct In *c = (In *)a; 

    struct In *d = (In *)b; 

    if(c->x != d->x) return c->x - d->x; 

    else return d->y - c->y; 

    qsort(s,100,sizeof(s[0]),cmp); 

    六、对字符串进行排序 

    struct In 

    int data; 

    char str[100]; 

    }s[100]; 

    // 按照结构体中字符串 str 的字典顺序排序 

    int cmp ( const void *a , const void *b ) 

    return strcmp( (*(In *)a)->str , (*(In *)B)->str ); 

    qsort(s,100,sizeof(s[0]),cmp); 

    七、计算几何中求凸包的 cmp 

    int cmp(const void *a,const void *B) // 重点 cmp 函数,把除了 1 点外的所有点,旋转角度排序 

    struct point *c=(point *)a; 

    struct point *d=(point *)b; 

    if( calc(*c,*d,p[1]) < 0) return 1; 

    else if( !calc(*c,*d,p[1]) && dis(c->x,c->y,p[1].x,p[1].y) < dis(d->x,d->y,p[1].x,p[1].y)) // 如果在一条直线上,则把远的放在前面 

    return 1; 

    else return -1; 

    }

    N!

    斯特灵公式是一条用来取n阶乘近似值的数学公式。一般来说,当n很大的时候,n阶乘的计算量十分大,所以斯特灵公式十分好用,而且,即使在

    n很小的时候,斯特灵公式的取值已经十分准确。

    公式为:

    这就是说,对于足够大的整数n,这两个数互为近似值。更加精确地:

    或者:

    求N!的位数:=log10(N!)+1=0.5*log10(2*PI*n)+n*log10(n/e);

    STL中list的使用:

    STL中的list就是一双向链表,可高效地进行插入删除元素。现总结一下它的操作。

    文中所用到两个list对象c1,c2分别有元素c1(10,20,30) c2(40,50,60)。还有一个list<int>::iterator citer用来指向c1或c2元素。

    list对象的声明构造():

    A. list<int>c0;  //空链表

    B. list<int>c1(3);  //建一个含三个默认值是0的元素的链表

    C. list<int>c2(5,2); //建一个含五个元素的链表,值都是2

    D. list<int>c4(c2); //建一个c2的copy链表

    E. list<int>c5(c1.begin(),c1.end());

    //c5含c1一个区域的元素[_First, _Last)。

    1. assign()分配值,有两个重载:

    c1.assign(++c2.begin(), c2.end()) //c1现在为(50,60)。

    c1.assing(7,4) //c1中现在为7个4,c1(4,4,4,4,4,4,4)。

    2. back()返回最后一元素的引用:

    int i=c1.back(); //i=30

    const int i=c2.back(); //i=60且不可修改

    3. begin()返回第一个元素的指针(iterator)

    citer=c1.begin(); // *citer=10

    list<int>::const_iterator cciter=c1.begin(); //*cciter=10且为const。

    4. clear()删除所有元素

    c1.clear(); //c1为空 c1.size为0;

    5. empty()判断是否链表为空

    bool B=c1.empty(); //若c1为空B=true;否则B=false;

    6. end()返回最后一个元素的下一位置的指针(list为空时end()=begin())

    citer=c1.end(); //*(--citer)=30;

    同begin()返回一个常指针,不能修改其中元素。

    7. erase()删除一个元素或一个区域的元素(两个重载)

    c1.erase(c1.begin()); // c1现为(20,30);

    c1.erase(++c1.begin(),c1.end()); // c1现为(10);

    8. front() 返回第一个元素的引用:

    int i=c1.front(); //i=10;

    const int i=c1.front(); //i=10且不可修改。

    9. insert()在指定位置插入一个或多个元素(三个重载):

    c1.insert(++c1.begin(),100); //c1(10,100,20,30)

    c1.insert(c1.begin(),2,200); //c1(200,200,20,30);

    c1.insert(++c1.begin(),c2.begin(),--c2.end());

    //c1(10,40,50,20,30);

    10. max_size()返回链表最大可能长度(size_type就是int型):

    list<int>::size_type i=c1.max_size(); //i=1073741823

    11. merge()合并两个链表并使之默认升序(也可改):

    c2.merge(c1); //c1现为空;c2现为c2(10,20,30,40,50,60)

    c2.merge(c1,greater<int>()); //同上,但c2现为降序

    12. pop_back()删除链表尾的一个元素

    c1.pop_back() //c1(10,20);

    13. pop_front()删除链表头的一元素

    c1.pop_front() //c1(20,30)

    14. push_back()增加一元素到链表尾

    c1.push_back(100) //c1(10,20,30,100)

    15. push_front()增加一元素到链表头

    c1.push_front(100) //c1(100,10,20,30)

    16. rbegin()返回链表最后一元素的后向指针(reverse_iterator or const)

    list<int>::reverse_iterator riter=c1.rbegin(); //*riter=30

    17. rend()返回链表第一元素的下一位置的后向指针

    list<int>::reverse_iterator riter=c1.rend(); // *(--riter)=10

    18. remove()删除链表中匹配值的元素(匹配元素全部删除)

    c1.remove(10);  //c1(20,30)

    19. remove_if()删除条件满足的元素(会遍历一遍链表)

    c1.remove_if( is_odd<int> () ); //c1(10,20,30) 

    //is_odd自己写(表奇数) 

    20. resize()重新定义链表长度(两重载):

    c1.resize(4) //c1(10,20,30,0)用默认值填补

    c1.resize(4,100) //c1(10,20,30,100)用指定值填补

    21. reverse()反转链表:

    c1.reverse(); //c1(30,20,10)

    22. size()返回链表中元素个数

    list<int>::size_type i=c1.size(); //i=3

    23. sort()对链表排序,默认升序(可自定义)

    c1.sort(); //c1(10,20,30)

    c1.sort(great<int>()); //c1(30,20,10)

    24. splice()对两个链表进行结合(三个重载)

    c1.splice(++c1.begin(),c2); 

    //c1(10,40,50,60,20,30) c2为空 全合并

    c1.splice(++c1.begin(),c2,++c2.begin()); 

    //c1(10,50,20,30) ; c2(40,60) 指定元素合并

    c1.splice(++c1.begin(),c2,++c2.begin(),c2.end()); 

    //c1(10,50,60,20,30); c2(40) 指定范围合并

    25. swap()交换两个链表(两个重载)

    c1.swap(c2); //c1(40,50,60);

    swap(c1,c2); //c1(40,50,60)

    26. unique()删除相邻重复元素(断言已经排序,因为它不会删除不相邻的相同元素)

    c1.unique(); 

    //假设c1开始(-10,10,10,20,20,-10)则之后为c1(-10,10,20,-10)

    c1.unique(mypred); //自定义谓词

    注意:insert函数是右结合方式

    。insert(j,*(--j))是最右向左进行的

    AVL 树

    2011-03-19 20:30

    AVL树又称为高度平衡的二叉搜索树,是1962年由两位俄罗斯数学家G.M.Adel’ son-Vel'sky和E.M.Landis提出的。引人它的目的,是为了提高二叉搜索树的效率,减 少树的平均搜索长度。为此,就必须向二又搜索树每插人一个新结点时调整树的结构,使 得二又搜索树保持平衡,从而尽可能降低树的高度,减少树的平均搜索长度。

    一、AVL树的定义:一棵ALV树或者是空树,或者是具有下列性质的二叉搜索树:它的左子树和右子树 都是AVL树,且左子树和右子树的高度之差的绝对值不超过1。

    图(a)给出的二叉搜索树不是AVL树,根的右子树的高度为l,而左子树的高度 为4。图(b)给出的二叉搜索树是AVL树。在图中每个结点旁边所注的数字结出 该结点右子树的高度减去左子树的高度所得的高度差。称这个数字为结点的平衡因子(balance factor)。 根据 AVL树的定义,任一结点的平衡因子只能取一1,0和 1。如果一个结点的平衡因子的绝对值大于1,则这棵二叉搜索树就失去了平衡,不再是AVL树了。‍

    如果一棵二叉搜索树是高度平衡的,它就成为AVL树,如果它有N个结点,其高度可保持在O(log2n),平均搜索长度也可保持在O(log2n)。

    二、平衡化旋转

    如果在一棵原本是平衡的二又搜索树中插入一个新结点,造成了不平衡。此时必须 调整树的结构,使之平衡化。平衡化旋转有两类:单旋转(左旋和右旋)和双旋转(左平衡 和右平衡)。给出加人了平衡化旋转操作的AVL一树的类声明如下。

    template <class Type> class AVLTree{ //平衡的一叉搜索树(AVL)类定义

    public:

    struct AVLNode { //AVL树结点的类定义

    Type data;

    AVLNode<Type>*left,* right;

    int balance ;

    AVLNode():left(NULL),right(NULL), balance(0){}

    AVLNode(Type d,AVLNode<Type>* l= NULL,AVLNode<Type>* r= NULL):

    data(d),left(l),right(r),balance(0){}

    };

    protected:

    Type RefValue; //插人结束的标志

    AVLNode<TyPe>* root ; //根结点的指针

    int Insert( AVLNOde<Type>*&tree, Type x);//插人

    void RotateLeft(AVLN0de<Type*>*Tree,AVLNOde<Type>*&NewTree);

    //左单旋转

    void RotateRight( AVLNode<Type>*Tree, AVLNode<Type>*& NewTree);

    //右单旋转

    void LeftBalance(AVLNode<Type>*&Tree); //左平衡化

    void RightBalance( AVLNode<Type>*& Tree); //右平衡化

    int Heisht( AVLNode<TyPe>* t) const; //求高度

    public:

    AVLTree():fOOt( NULL){ } //构造函数:构造一棵空 AVL树

    AVLNode(Type Ref): RefValue(Ref),root(NULL){}

    //构造函数:构造非空 AVL树

    int Insert(Type){return Insert(root,x);}

    int Height() Const;



      每当插人一个新结点时,AVL树中相关结点的平衡状态会发生改变。因此,在插人 一个新结点后,需要从插入位置沿通向根的路径回溯,检查各结点左、右子树的高度差。 如果在某一结点发现高度不平衡,停止回溯。从发生不平衡的结点起,沿刚才回溯的路径 取直接下两层的结点。如果这三个结点处于一条直线上,则采用单旋转进行平衡化;如果 这三个结点处于一条折线上,则采用双旋转进行平衡化。
      单旋转可按其方向分为左单旋转和右单旋转,其中一个是另一个的镜像,其方向与不 平衡的形状相关。而双旋转分为先左旋后右旋和先右旋后左旋两类。

    1.左单旋转(rotate left)
      如果在插入新结点之前AVL树的形状如图7.18(a)所示。图中的大写字母用来指 明结点,矩形框表示结点的子树,其中的字母h给出子树的高度。若h=一1,则B,C和 D都是空指针;若h≥0,则这三个结点在树中实际存在。此时树满足AVL树的条件,是 高度平衡的二叉搜索树。接下来,如果在子树E中插人一个新结点,该子树的高度增加1 导致结点A的平衡因子变成 十2,如图 7.18(b)所示,出现不平衡。

    图7.18 左单旋转前后树的变化

      沿插人路径检查三个结点A,C和E。它们处于一条方向为“\”的直线上,需要做左 单旋转:以结点C为旋转轴,让结点A反时针旋转成为C的左子女,C代替原来A的位 置,原来C的左子女D转为A的右子女,旋转后的形状如图7.18(C)所示。通过检查各 个结点的平衡因子可知,树又恢复了平衡。

    2.右单旋转(rotate right)
      如果一棵 AVL树如图 19(a)所示,在结点 B的左子树D上插入新结点使其高度 由h增加到h+l,导致结点A的平衡因子由一1增加到一2,造成了不平衡,见图 19(b)。为使树恢复平衡,从A沿刚才的插人路径连续取3个结点A,B和D,它们处于 一条方向为“/’的直线上,因此需要做右单旋转,以结点B为旋转轴,将结点A顺时针向 下旋转成为B的右子女,结点B代替原来结点A的位置,原来结点B的右子女转为结点 A的左子女。从而使树又达到平衡。

    图19 右单旋转前后树的变化

    3.先左后古双旋转(rotation left right)
      双旋转总是考虑3个结点。设给出一棵AVL树,如图20(a)所示。图7.20中用 矩形框表示的所有子树都是AVL树。结点B和E的平衡因子为0而结点A的平衡因 子为一1。子树的高度h至少等于1。现在假设我们在子树F或G中插人一个新结点,则 该子树的高度增加 1,如图 20(b)将新结点插人到子树F中。此时结点A的平衡因子 变为一2,发生了不平衡。从结点A起沿插人路径选取3个结点A,B和E,它们位于一 条形如“ <”的折线上,因此需要进行先左后右的双旋转。首先以结点E为旋转轴,将结 点B反时针旋转,以E代替原来B的位置,使得B成为E的左子女,原来E的左子女F 转为B的右子女。参看图20(C),这恰为前面介绍的左单旋转。接下来,再以结点E为 旋转轴,将结点A顺时针旋转,使得A成为E的右子女,原来E的右子女G转为A的左 子女。这样又恢复了树的平衡。

    图20 先左后右双旋转

    4.先右后左双旋转(rotation right left)
      如图21(a)所示。结点D和C的平衡因子为0而结点A的平衡因子为豆。子树 的高度h至少等于1。现在在子树F或G中插人一个新结点,则该子树的高度增加1,例如图21(b)将新结点插人到子树G中。此时结点A的平衡因子变为2,发生了不平衡。 从结点 A起沿插人路径选取3个结点 A,C和D,它们位于一条形如“ >”的折线上,因此 需要进行先右后左的双旋转。首先做右单旋转:以结点D为旋转轴,将结点C顺时针旋转,以D代替原来C的位置,使得C成为D的右子女,原来D的右子女G转为C的左子女。参看图7.21(c)。接下来做左单旋转:以结点D为旋转轴,将结点A反时针旋转,使得A成为D的左子女,原来D的左子女F转为A的右子女。这样恢复了树的平衡,如图21(d)所示。

    图21 先右后左双旋转

    三、AVL树的插入和删除

    在向一棵本来是高度平衡的AVL树中插人一个新结点时,应判断从插入结点到根 的路径L各结点的平衡因子的变化。如果树中某个结点的平衡因子的绝对值 |balance| >1,则出现了不平衡,需要从离插人结点最近的发生不平衡的结点沿插人路径做平衡化处理。
      通常,AVL树的建立从一棵空树开始。通过输人一系列对象的关键码,根据前面所 讲的二叉搜索树的插人方法插人新结点。每插入一个结点后就应判断从该结点到根的路 径上是否有结点发生不平衡,如果有不平衡情况,利用平衡旋转方法进行树的调整,逐步建立AVL树。设输人关键码序列为{16,3,7,11,9,26,18,14,15},则插人和调整过程如图7.22所示。



      为了在AVL树上执行删除,同样需要考虑平衡化旋转问题。给出步骤如下:
    (1)如果被删结点X最多只有一个子女,那么问题比较简单。如果被删结点X有两 个子女,首先搜索X在中序次序下的直接前驱y(同样可以找直接后继)。再把结点y的 内容传送给结点x,现在问题转移到删除结点y,把结点y当作被删结点x。

    (2)将结点x从树中删去。因为结点X最多有一个子女,可以简单地把X的双亲? 点中原来指向x的指针改指到这个子女结点;如果结点X没有子女,x双亲结点的相应指 针置为NULL。然后将原来以结点工为根的子树的高度减1,并沿X通向根的路径反向 追踪高度的这一变化对路径上各个结点P的影响,进行平衡化处理。
    casel:当前结点 P的平衡因子为 0。如果它的左子树或右子树被缩短,则它的平衡 因子改为 1或一1。参看图 7.23的 casel。
    case2:结点 p的平衡因子不为 0,且较高的子树被缩短,则 p的平衡因子改为 0。参 看图 7.23的 case2。
    case3:结点 P的平衡因子不为 0,且较矮的子树又被缩短,则在结点 P发生不平衡。 我们将进行平衡化旋转来恢复平衡。令P的较高的子树的根为q(该子树未被缩短),根 据。的平衡因子,有如下3种平衡化操作。
    case 3a:如果 Q的平衡因子为 0,则执行一个单旋转来恢复结点 P的平衡,如图 7.23 中 case 3a所示。
    case 3b:如果 q的平衡因子与 P的平衡因子相同,则执行一个单旋转来恢复平衡,结 点 p和q的平衡因子均改为 0。如图 7.23中 case 3b所示。

    case 3c:如果 p与q的平衡因子相反,则执行一个双旋转来恢复平衡,先围绕q转再 围绕产转。新的根结点的平衡因子置为0,其他结点的平衡因子相应处理。参看图7.23 ·中的 case 3c。
      在 case 3a, 3b和 3c的情形中,旋转的方向取决于是结点户的哪一棵子树被缩短,在图 7.23中只是给出了某些可能情况。图7.24给出删除一个结点时做平衡化旋转的示例。

    四、AVL的高度

    若设在新结点插人前AVL树的高度为h,结点个数为n,则插人一个新结点的时间 是O(h)。这与一般的二叉搜索树相同。但对于一棵不一定平衡的三叉搜索树来说,树的 高度最大可能是h=n-1,因此,最坏情况下,在一般二又搜索树中插入一个新结点所需 的时间为o(n)。那么,对于AVL树来说,h应当是多大呢? 
    若设Nh是高度为h的AVL树的最小结点数。在最坏情况下,根的一棵子树的高度为h-l,另一棵子树的高度为h-2,这两棵子树也是高度平衡的。因此有

      二叉搜索树适合于组织在内存中的较小的索引(或目录)。对于存放在外存中的较大 的文件系统,用二叉搜索树来组织索引就不太合适了。若以结点为内外存交换的单位,则在搜索过程中为找到所需的关键码,需对外存进行log2n次访问,这是很费时的。因此在文件检索系统中大量使用的是用B树或B十树做文件索引。

    堆排序

    1、 堆排序定义
     n个关键字序列Kl,K2,…,Kn称为堆,当且仅当该序列满足如下性质(简称为堆性质):
     (1) ki≤K2i且ki≤K2i+1 或(2)Ki≥K2i且ki≥K2i+1(1≤i≤ )

     若将此序列所存储的向量R[1..n]看做是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。
    【例】关键字序列(10,15,56,25,30,70)和(70,56,30,25,15,10)分别满足堆性质(1)和(2),故它们均是堆,其对应的完全二叉树分别如小根堆示例和大根堆示例所示。



    2、大根堆和小根堆
     根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最小者的堆称为小根堆。
     根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者,称为大根堆。
    注意:
     ①堆中任一子树亦是堆。
      ②以上讨论的堆实际上是二叉堆(Binary Heap),类似地可定义k叉堆。

    3、堆排序特点
     堆排序(HeapSort)是一树形选择排序。
     堆排序的特点是:在排序过程中,将R[l..n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系【参见二叉树的顺序存储结构】,在当前无序区中选择关键字最大(或最小)的记录。

    4、堆排序与直接插入排序的区别
     直接选择排序中,为了从R[1..n]中选出关键字最小的记录,必须进行n-1次比较,然后在R[2..n]中选出关键字最小的记录,又需要做n-2次比较。事实上,后面的n-2次比较中,有许多比较可能在前面的n-1次比较中已经做过,但由于前一趟排序时未保留这些比较结果,所以后一趟排序时又重复执行了这些比较操作。
     堆排序可通过树形结构保存部分比较结果,可减少比较次数。

    5、堆排序
    堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。

    (1)用大根堆排序的基本思想
    ① 先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区
    ② 再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key
    ③ 由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。
    ……
    直到无序区只有一个元素为止。

    (2)大根堆排序算法的基本操作:
    ① 初始化操作:将R[1..n]构造为初始堆;
    ② 每一趟排序的基本操作:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。
    注意:
    ①只需做n-1趟排序,选出较大的n-1个关键字即可以使得文件递增有序。
    ②用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任何时刻,堆排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止。

    (3)堆排序的算法:
    void HeapSort(SeqIAst R)
    { //对R[1..n]进行堆排序,不妨用R[0]做暂存单元
    int i;
    BuildHeap(R); //将R[1-n]建成初始堆
    for(i=n;i>1;i--){ //对当前无序区R[1..i]进行堆排序,共做n-1趟。
    R[0]=R[1];R[1]=R[i];R[i]=R[0]; //将堆顶和堆中最后一个记录交换
      Heapify(R,1,i-1); //将R[1..i-1]重新调整为堆,仅有R[1]可能违反堆性质
    } //endfor
    } //HeapSort

    (4) BuildHeap和Heapify函数的实现
     因为构造初始堆必须使用到调整堆的操作,先讨论Heapify的实现。
    ① Heapify函数思想方法
     每趟排序开始前R[l..i]是以R[1]为根的堆,在R[1]与R[i]交换后,新的无序区R[1..i-1]中只有R[1]的值发生了变化,故除R[1]可能违反堆性质外,其余任何结点为根的子树均是堆。因此,当被调整区间是R[low..high]时,只须调整以R[low]为根的树即可。
    "筛选法"调整堆
      R[low]的左、右子树(若存在)均已是堆,这两棵子树的根R[2low]和R[2low+1]分别是各自子树中关键字最大的结点。若R[low].key不小于这两个孩子结点的关键字,则R[low]未违反堆性质,以R[low]为根的树已是堆,无须调整;否则必须将R[low]和它的两个孩子结点中关键字较大者进行交换,即R[low]与R[large](R[large].key=max(R[2low].key,R[2low+1].key))交换。交换后又可能使结点R[large]违反堆性质,同样由于该结点的两棵子树(若存在)仍然是堆,故可重复上述的调整过程,对以R[large]为根的树进行调整。此过程直至当前被调整的结点已满足堆性质,或者该结点已是叶子为止。上述过程就象过筛子一样,把较小的关键字逐层筛下去,而将较大的关键字逐层选上来。因此,有人将此方法称为"筛选法"。
      具体的算法【参见教材】

    ②BuildHeap的实现
      要将初始文件R[l..n]调整为一个大根堆,就必须将它所对应的完全二叉树中以每一结点为根的子树都调整为堆。
      显然只有一个结点的树是堆,而在完全二叉树中,所有序号 的结点都是叶子,因此以这些结点为根的子树均已是堆。这样,我们只需依次将以序号为 , -1,…,1的结点作为根的子树都调整为堆即可。
      具体算法【参见教材】。

    5、大根堆排序实例
     对于关键字序列(42,13,24,91,23,16,05,88),在建堆过程中完全二叉树及其存储结构的变化情况参见【动画演示】。

    6、 算法分析
     堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成,它们均是通过调用Heapify实现的。
      堆排序的最坏时间复杂度为O(nlgn)。堆排序的平均性能较接近于最坏性能。
     由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。
     堆排序是就地排序,辅助空间为O(1),
     它是不稳定的排序方法。

    C++ 折半查找(二分查找)算法

    /*二分查找(折半查找)算法
    折半查找法也称为二分查找法,它充分利用了元素间的次序关系
    采用分治策略,可在最坏的情况下用O(log n)完成搜索任务。
    前提:要查找数组需为有序数组。
    将n个元素分成个数大致相同的两半,取a[n/2]与欲查找的x作比较,如果x=a[n/2]则找到x,算法终止。
    如果x<a[n/2],则我们只要在数组a的左半部继续搜索x(这里假设数组元素呈升序排列)。
    如果x>a[n/2],则我们只要在数组a的右半部继续搜索x。 
    二分搜索法的应用极其广泛,而且它的思想易于理解。
    第一个二分搜索算法早在1946 年就出现了,但是第一个完全正确的二分搜索算法直到1962年才出现。
    Bentley在他的著作《Writing Correct Programs》中写道,90%的计算机专家不能在2小时内写出完全正确的二分搜索算法。
    问题的关键在于准确地制定各次查找范围的边界以及终止条件的确定,正确地归纳奇偶数的各种情况,其实整理后可以发现它的具体算法是很直观的。 
    */


    #include <iostream>

    using namespace std;

    int Bisearch(int data[],int x,int begin,int end)
    {//折半查找算法实现
    if (begin>end)
    {//判断是不是只有 一个元素可以比较
    return -1;
    exit(0);
    }

    int mid = (begin+end)/2;

    if (x==data[mid])
    {//如果X的值与data[mid]的值相等,则查找成功,本函数执行完毕
    //cout<<"The mid:"<<mid<<endl;
    return mid;
    exit(0); 
    }

    else if (x<data[mid])
    {//当X <data[mid] 时,从左面 开始查找数据X ,用递归实现
    return Bisearch(data,x,begin,mid-1);
    }

    else
    {//如果x<data[mid]的情况下,即往数组的右半部分扫描数组
    return Bisearch(data,x,mid+1,end);
    }
    }

    int main()
    {
    const int length = 10;
    int data[length];

    for (int i=0;i<length;++i)
    {
    data[i] = i;
    }

    cout<<"数组元素有:"<<endl;

    for (int i=0;i!=length;++i)
    {
    cout<<data[i]<<" ";
    }
    cout<<endl;

    int x;
    cout<<"输入要查找的元素:"<<endl;
    cin>>x;
    int location=Bisearch(data,x,0,9);
    //cout<<"The location:"<<location<<endl;
    if(location==-1)
    {
    cout<<"Error! 未找到元素!"<<endl;
    }
    else
    {
    cout<<"要查找的元素: "<<x<<" 的位置在第 "<<location+1<<"个位置!"<<endl;
    }
    }

    hash表

    hash表,有时候也被称为散列表。个人认为,hash表是介于链表和二叉树之间的一种中间结构。链表使用十分方便,但是数据查找十分麻烦;二叉树中的数据严格有序,但是这是以多一个指针作为代价的结果。hash表既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。

    打个比方来说,所有的数据就好像许许多多的书本。如果这些书本是一本一本堆起来的,就好像链表或者线性表一样,整个数据会显得非常的无序和凌乱,在你找到自己需要的书之前,你要经历许多的查询过程;而如果你对所有的书本进行编号,并且把这些书本按次序进行排列的话,那么如果你要寻找的书本编号是n,那么经过二分查找,你很快就会找到自己需要的书本;但是如果你每一个种类的书本都不是很多,那么你就可以对这些书本进行归类,哪些是文学类,哪些是艺术类,哪些是工科的,哪些是理科的,你只要对这些书本进行简单的归类,那么寻找一本书也会变得非常简单,比如说如果你要找的书是计算机方面的书,那么你就会到工科一类当中去寻找,这样查找起来也会显得麻烦。

    不知道这样举例你清楚了没有,上面提到的归类方法其实就是hash表的本质。下面我们可以写一个简单的hash操作代码。

    a)定义hash表和基本数据节点

    [cpp] view plaincopyprint?

    1. typedef struct _NODE 
    2. int data; 
    3. struct _NODE* next; 
    4. }NODE; 
    5. typedef struct _HASH_TABLE 
    6. NODE* value[10]; 
    7. }HASH_TABLE; 


    b)创建hash表

    [cpp] view plaincopyprint?

    1. HASH_TABLE* create_hash_table() 
    2. HASH_TABLE* pHashTbl = (HASH_TABLE*)malloc(sizeof(HASH_TABLE)); 
    3. memset(pHashTbl, 0, sizeof(HASH_TABLE)); 
    4. return pHashTbl; 


    c)在hash表当中寻找数据

    [cpp] view plaincopyprint?

    1. NODE* find_data_in_hash(HASH_TABLE* pHashTbl, int data) 
    2. NODE* pNode; 
    3. if(NULL == pHashTbl) 
    4. return NULL; 
    5. if(NULL == (pNode = pHashTbl->value[data % 10])) 
    6. return NULL; 
    7. while(pNode){ 
    8. if(data == pNode->data) 
    9. return pNode; 
    10. pNode = pNode->next; 
    11. return NULL; 


    d)在hash表当中插入数据

    1. STATUS insert_data_into_hash(HASH_TABLE* pHashTbl, int data) 
    2. NODE* pNode; 
    3. if(NULL == pHashTbl) 
    4. return FALSE; 
    5. if(NULL == pHashTbl->value[data % 10]){ 
    6. pNode = (NODE*)malloc(sizeof(NODE)); 
    7. memset(pNode, 0, sizeof(NODE)); 
    8. pNode->data = data; 
    9. pHashTbl->value[data % 10] = pNode; 
    10. return TRUE; 
    11. if(NULL != find_data_in_hash(pHashTbl, data)) 
    12. return FALSE; 
    13. pNode = pHashTbl->value[data % 10]; 
    14. while(NULL != pNode->next) 
    15. pNode = pNode->next; 
    16. pNode->next = (NODE*)malloc(sizeof(NODE)); 
    17. memset(pNode->next, 0, sizeof(NODE)); 
    18. pNode->next->data = data; 
    19. return TRUE; 


    e)从hash表中删除数据

    [cpp] view plaincopyprint?

    1. STATUS delete_data_from_hash(HASH_TABLE* pHashTbl, int data) 
    2. NODE* pHead; 
    3. NODE* pNode; 
    4. if(NULL == pHashTbl || NULL == pHashTbl->value[data % 10]) 
    5. return FALSE; 
    6. if(NULL == (pNode = find_data_in_hash(pHashTbl, data))) 
    7. return FALSE; 
    8. if(pNode == pHashTbl->value[data % 10]){ 
    9. pHashTbl->value[data % 10] = pNode->next; 
    10. goto final; 
    11. pHead = pHashTbl->value[data % 10]; 
    12. while(pNode != pHead ->next) 
    13. pHead = pHead->next; 
    14. pHead->next = pNode->next; 
    15. final: 
    16. free(pNode); 
    17. return TRUE; 
  • 相关阅读:
    JavaScript对象的几种创建方式?
    TCP 三次握手,四次挥手
    常用的状态码
    前后端分离的接口规范
    京东架构师:日均 5 亿查询量的ElasticSearch架构如何设计?
    [转] 谈谈Spring中都用到了那些设计模式?
    [转]Post和Get的区别
    [转]17个常用的JVM参数
    从入门到熟悉HTTPS的9个问题
    布式事务和解决方案理论
  • 原文地址:https://www.cnblogs.com/jeff-wgc/p/4484702.html
Copyright © 2011-2022 走看看