《编程珠玑》第11、12、13章读书笔记

第三部分 应用

 第11章 排序

    问题:使用库排序函数并非总是有效的,因此在某些情况下程序员要自己编写排序函数。

 插入排序:大多纸牌游戏的玩家都采用插入排序来排列手中的纸牌。他们保持已发到手中的牌有序,当拿到一张新牌时,即将其插入合适的位置,

    伪代码

        for i = [1,n)

            /* invariant: x[0..n-1] is sorted */

            /* goal: sift x[i] down to its proper place in x[0..i] */

    完整的程序:

        for i = [1,n)

            for(j = i; j > 0 && x[j-1] > x[j]; j--)

                swap(j-1,j)

 

    优化考虑

    >>swap函数调用

直接将函数体写入内循环。(许多优化编译器会实现该功能)

[加速系数= 3]

for i = [1,n)

            for(j = i; j > 0 && x[j-1] > x[j]; j--)

            {

                t = x[j]; x[j] = x[j-1]; x[j-1] = t;

}

 

    >>内循环的t重复赋值

        将两个含t的赋值语句移出内循环,减少重复的赋值步骤。

        [加速系数= 1.15]

for i = [1,n)

{

    t = ***>

            for(j = i; j > 0 && x[j-1] > x[j]; j--)

            {

                x[j] = x[j-1];

}

x[j-1] = t;

 

}

最坏情况下,该算法的运行时间与n2成正比,而如果输入数组几乎是有序的,那么每个元素移动的距离都很短,插入排序的速度会快很多;

 
快速排序:分治算法:排序数组时,将数组分成两个小部分,然后对他们递归排序。

伪代码:递归结束的条件是待排序部分的元素个数小于2.

        void qsort(l,u)

            if(l >= u) then

                /* at most one element, do nothing */

                Return

/* goal: partition array around a particular value, which is eventually placed in its correct postion p */

qsort(l,p-1)

qsort(p+1,u)

优化考虑

    >>双向划分: 避免非随机输入时快速排序的性能差的问题

假设i和j是待划分数组的两端,主循环中有两个内循环,第一个内循环将i向右移过小元素,遇到大元素时停止;第二个内循环将j向左移过大元素,遇到小元素时停止,然后主循环判断两个下标是否交叉并交换他们的值。

[最坏情况 : nlog2n]

A:当元素相同时代码如何处理呢?当遇到相同元素时停止扫描,并交换i和j的值。这样做交换次数增加了,但将所有元素都相同的最坏情况变成了差不多需要nlog2n次。

B:如果数组已经按升序排好了,可以通过随机选择划分元素提交性能。swap(l, randint(l,u));

C:可使用插入排序来排序很小的子数组提高效率。设置cutoff阈值,其确定可以通过对比其在[1,100]上每个可能取值的运行时间来确定。

D:展开循环体内的swap函数的代码

>>swap函数调用

同上一节。

完整代码:

    void qsort4(l,u)

        if u - l < cutoff

                return

            swap(l, randint(l,u))

            t = x[l]; i = l; j = u + 1

            loop

                do i++; while i <= u && x[i] < t

                do i--; while x[i] > t

                if(i > j)

break;

                    temp = x[i]; x[i] = x[j]; x[j] = temp;

                swap(l,j)

                qsort4(l, j-1)

                qsort4(j+1, u)

C++标准库函数sort由于具有最简单的接口,平均运行时间最少;


第12章 取样问题

 本章针对随机取样问题,讨论了解决问题的一般思路。

问题描述:假设有一个能返回很大的随机整数(远远大于n,m)的函数bigrand(),以及一个能返回i..j范围内均匀选择的随机整数的函数randint(i,j),如何实现选择并输出0~n-1范围内的m个随机整数的有序列表。从概率的角度,希望没有重复的有序选择,其中每个选择出现的概率相等。

    方法1:Knuth通过概率证明的方法(按序访问整数,可保证输出结果有序。)(从r个剩余的整数中选出s个,可以以概率s/r选择下一个数。)

完整代码:

    void genknuth(int m , int n){

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

/* select m of remaining n-i */

            if((bigrand() % (n-i)) < m){

                cout << i << “\n”

m--;

}

}

算法的运行时间和n成正比。

    方法2:在一个初始集为空的集合内插入随机整数,直到个数足够(利用C++标准模板库,用set表示集合)

完整代码:

    void gensets(int m , int n){

        set<int> S;

        while(S.size() < m)

            S.insert(bigrand() % n);

        set<int>::iterator i;

        for(i = S.begin; i != S.end(); ++i)

            cout << *i <<”\n”;

}

插入操作时间复杂度为O(longm),遍历集合需要O(m),所以完成程序时间复杂度为O(mlogm);但是该数据结构空间开销比较大。

方法3: 打乱包含n个元素的数组顺序,前m个元素排序输出即可

void genshuf(int m , int n){

        int i, j;

int *x = new int[n];

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

            x[i] = i;

        for(int i = 0; i < m; i++){

            j = randint(i, n-1);

            int t = x[i]; x[i] = x[j]; x[j] = t;

}

        for(i = 0; i < m; i++)

            cout << x[i] <<”\n”;

}//空间O(n),时间O(n+mlogm)

这一方法需要O(n)的空间与时间。

其他解决方案概述:

A:当n比较大,并且m和n比较接近时,可以生成(n-m)个有序随机样板,然后输出不在样本中的整数。

B:当m=1000万,n=231时,可以先生成1100万个整数,然后排序并对其扫描删除重复的元素,最后得到有1000万个元素的有序样本。
小结:解决问题的一般思路为:正确理解所遇到的问题>>提炼出抽象问题>>考虑尽可能多的解法(伪代码表示控制流,抽象数据类型表示关键数据结构)>>实现一种解决方案>>回顾(研究改进)

 

第13章 搜索
研究问题:在没有其他相关数据的情况下,如何存储一组整数?

本章主要介绍,存储按序输出的容器或者说存放集合的方法。并实现按序插入,按序输出。

1)set容器  set<int> S;(接口)

set容器实现插入元素,并有序输出;

class  IntSetSTL{

private:

    set<int> S;

public:

    IntSetSTL(int maxelements, int maxval){}

    int size(){return S.size;}

    void insert(int t){S.insert(t);}

    void report(int *v)

    {  int j = 0;

       set<int>::iterator i;

       for(i = S.begin(); i != S.end(); ++i)

           v[j++] = *i;

}

}

2)数组(类似于插入排序)(线性结构:事先知道集合大小)

思路:初始化时候,取一个很大元素作为哨兵,哨兵永远置于元素最后。

插入时候遇到重复元素,返回;

遇到比自己大的第一个元素,将该元素以及以后的元素后移一位,最后将该元素插入该位置;

3)链表(线性结构:事先不知道集合大小)

思路1:【递归】调用的时候,p->next=rinsert(p->next,t);如果p-val > t则p =new node(t,p)

说明第一个大于t的p被作为新节点(val=t)的next

思路2:【存储分配】构造函数只分配一个具有m个结点的块,insert根据需要使用这些空间;而不是每次插入分配一个新的结点

时间开销:存储分配的时间开销要比大多数简单运算高出两个数量级;

空间开销:若将多个结点分配为一个块,每个结点8个字节;若分别为这些结点分配空间,每个结点48个字节;

思路3:【链表非递归】主要是找到第一个大于t的节点,然后插入到其前面。

提高效率的方法:高速缓存(加速系数1.1)和消除递归(加速系数为5)。

链表结构效率比数组低的原因:大链表必须将8字节的结点读入高速缓存以访问4字节的整数,另外数组访问数据时具有较好的预见性,而链表的访问模式则可能导致在内存空间的来回跳跃。

4)二分搜索树(类似链表)

如果事先知道集合的大小,那么数组这种结构来比较适合实现集合。因为数组是有序的,所以能用二分查找建立一个运行时间为O(logn)的成员函数。

简单的二分查找树避免了STL所使用的复杂的平衡方法,因此,它会稍微快一些,同时使用的空间也更少一些。最重要的是它一下子就分配所有的结点。(自己管理分配)这就大大降低了树的空间需求,运行时间降低了。

       5)专门用于整数处理的数据结构

        位向量的私有数据和函数:

enum { BITSPERWORD = 32, SHIFT = 5, MASK = 0x1F };

    int    n, hi, *x;

    void set(int i)  {        x[i>>SHIFT] |=  (1<<(i & MASK)); }

    void clr(int i)  {        x[i>>SHIFT] &= ~(1<<(i & MASK)); }

    int  test(int i) { return x[i>>SHIFT] &   (1<<(i & MASK)); }

 

IntSetBitVec(int maxelements, int maxval)

    {    hi = maxval;

        x = new int[1 + hi/BITSPERWORD];

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

            clr(i);//所有的位都置零

        n = 0;

    }

    int size() { return n; }

    void insert(int t)

    {    if (test(t))

            return;

        set(t);

        n++;

    }

    void report(int *v)

    {    int j=0;

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

            if (test(i))

                v[j++] = i;

    }

结合链表和位向量的优点,每一个箱表示一定范围的整数,并用链表链接起来。链表插入采用有序插入;

int    n, bins, maxval;

    struct node {

        int val;

        node *next;

        node(int v, node *p) { val = v; next = p; }

    };

    node **bin, *sentinel;

 

void insert(int t){    int i = t / (1 + maxval/bins);  //安全映射到某个箱子中

        bin[i] = rinsert(bin[i], t);        //类似于前面链表中的rinsert

    }

    void report(int *v)  //将对应的链表代码按顺序应用到每个箱上

    {    int j = 0;

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

            for (node *p = bin[i]; p != sentinel; p = p->next)

                v[j++] = p->val;

    }

小结:库的作用:当涉及数据结构问题时,可以使用库函数解决特定问题,提高运行速度。

空间的重要性:链表因为非顺序存储,效率比数组更低。

            代码调优方法:分配大的内存块替代通用的内存分配。

#笔记##读书笔记#
全部评论

相关推荐

头像
04-09 14:29
Java
点赞 评论 收藏
转发
点赞 收藏 评论
分享
牛客网
牛客企业服务