一、简介
通过构造可生成一个随机整数的有序集合(不允许重复)来引出问题的讨论。C++相关的代码使用了set数据结构,具体代码如下所示:
void gensets(int m, int maxval) { int *v = new int[m]; IntSetImp S(m, maxval); while(S.size() < m) S.insert(bigrand() % maxval); S.report(v); for(int i=0;i < m;i++) cout<<v[i]<<"\n"; }
二、链表
而后通过描述线性结构,分别以类似插入排序的数组及链表查找插入数据的形式实现分析了数据的插入顺序插入操作。这里只介绍下链表的相关实现,下面是链表的递归插入实现:
思路:node *rinsert(node *p, int t)是关键函数,注意调用的时候,p->next=rinsert(p->next,t); 如果小于则一直等于next,如果p-val > t 则 p =new node(t,p);说明第一个大于t的p被作为新节点(val=t)的next,代码实现如下:
class IntSetList { private: int n; struct node { int val; node *next; node(int i, node *p) { val = i; next = p; } }; node *head, *sentinel; node *rinsert(node *p, int t) //递归插入函数 { if (p->val < t) { p->next = rinsert(p->next, t); } else if (p->val > t) { p = new node(t, p); n++; } return p; } public: IntSetList(int maxelements, int maxval) //初始化函数时候,将哨兵置于第一个 { sentinel = head = new node(maxval, 0); n = 0; } int size() { return n; } void insert(int t) { head = rinsert(head, t); } //调用递归插入函数 void insert2(int t) { node *p; if (head->val == t) return; if (head->val > t) { head = new node(t, head); n++; return; } for (p = head; p->next->val < t; p = p->next) ; if (p->next->val == t) return; p->next = new node(t, p->next); n++; } void insert3(int t)//类似数组的插入 { node **p; for (p = &head; (*p)->val < t; p = &((*p)->next))//找到第一个大于等于p的节点 ; if ((*p)->val == t) return; *p = new node(t, *p);//创建节点的同时,将节点插入到正确位置 n++; } void report(int *v)//输出整个链表 { int j = 0; for (node *p = head; p != sentinel; p = p->next) v[j++] = p->val; } };
第二种实现方法(链表非递归):主要是找到第一个大于t的节点,然后插入到其前面。实现代码如下:
class IntSetList2 { private: int n; struct node { int val; node *next; }; node *head, *sentinel, *freenode; public: IntSetList2(int maxelements, int maxval)//最大元素个数和哨兵 { sentinel = head = new node; sentinel->val = maxval; freenode = new node[maxelements]; n = 0; } int size() { return n; } void insert(int t) { node **p; for (p = &head; (*p)->val < t; p = &((*p)->next)) ; if ((*p)->val == t) return; freenode->val = t; //将t插入到第一个大于t的元素的前面 freenode->next = *p; *p = freenode++; n++; } void report(int *v) { int j = 0; for (node *p = head; p != sentinel; p = p->next) v[j++] = p->val; } };
递归调用实现的链表插入,除了递归调用的开销外,rinsert函数的递归深度就是找到元素的位置,即O(n)。递归全部结束后,代码将初值赋给几乎所有的指针。当将其改为非递归实现版本后,运行时间极虎降低为原来的三分之一。
三、二分搜索树
利用二分搜索树来实现整数的查找及插入,递归实现如下:
class IntSetBST { private: int n, *v, vn; struct node { int val; node *left, *right; node(int v) { val = v; left = right = 0; } }; node *root; node *rinsert(node *p, int t) { if (p == 0) { p = new node(t); n++; } else if (t < p->val) { p->left = rinsert(p->left, t); } else if (t > p->val) { p->right = rinsert(p->right, t); } // do nothing if p->val == t return p; } void traverse(node *p)//中序遍历输出 { if (p == 0) return; traverse(p->left); v[vn++] = p->val; traverse(p->right); } public: IntSetBST(int maxelements, int maxval) { root = 0; n = 0; } int size() { return n; } void insert(int t) { root = rinsert(root, t); } void report(int *x) { v = x; vn = 0; traverse(root); } };
四、专门用于整数处理的数据结构
利用位图的思想,位图数据中特定索引位记录此数是否存在,通过清除和设置相应的bit位来标记相应的数是否在集合中。例如x的第一个字节的第三个bit若为1,则表示数组2在集合中。代码实现如下所示:
class IntSetBitVec { private: 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)); } public: 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; } };
解释:其中i>>SHIFT 相当于 i/32得到对应数组下标,二进制右移5位相当于十进制除以32,i&MASK相当于 i mod 32,因为每一个数32位。最大只能左移32位。1<<(i&MASK)相当于获得2的(i&MASK)次幂,就是1左移(i&MASK)位,i=[0,31] 标记在数组第一字节,i=[32,63] 标记在数组第二字节。
五、如何输出一个数的二进制位
代码实现如下:
#include <iostream> using namespace std; int main() { int n; cout << "input n:"; cin >> n; for(int i=1; i<=32; i++) { cout << ( n < 0 ? 1 : 0 ); //如果首位为1则为负数 所以小于0 n = n<<1;//左移一位 } cout << endl; return 0; }
六、箱
箱是一个集合了链表和位向量优点的数据结构。其思路是每一个箱表示一定范围的整数,并用链表链接起来。链表插入采用有序插入。实现代码如下:
class IntSetBins { private: int n, bins, maxval; struct node { int val; node *next; node(int v, node *p) { val = v; next = p; } }; node **bin, *sentinel; node *rinsert(node *p, int t)//递归 插入结点到合适地方 { if (p->val < t) { p->next = rinsert(p->next, t); } else if (p->val > t) { p = new node(t, p); n++; } return p; } public: IntSetBins(int maxelements, int pmaxval) { bins = maxelements; maxval = pmaxval; bin = new node*[bins]; sentinel = new node(maxval, 0); for (int i = 0; i < bins; i++) bin[i] = sentinel; n = 0; } int size() { return n; } void insert(int t) { int i = t / (1 + maxval/bins); // CHECK ! 映射到某个箱子中 bin[i] = rinsert(bin[i], t); } 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; } };
七、习题答案
(1)习题1:Floyd算法的IntSet实现中只需要在插入数据前保存原先的Set大小值,然后插入数据,再判断Set大小是否改变,若改变,则插入成功,若未改变,则插入当前的j值。具体见书中的P214。
(2)习题5:如何避免多次调用存储分配器来提高效率:为了用一次分配来取代多次分配,需要有一个指向下一个可用节点的指针:
node * freenode;
在构造类的时候就分配出足够的空间:
freenode = new node[maxelems];
然后在插入函数中根据需要加以使用:
if(p == 0) { p = freenode++; p->val = t; p->left = p->right = 0; n++ } else if ...
同样的方法可以应用到箱中。
(3)习题7:如何将哨兵用于二分搜索树:把以前null指针都指向哨兵节点,哨兵在构造函数中进行初始化,插入代码先将目标值t放入哨兵节点,然后用一个指向指针的指针自顶向下遍历数直到找到t。代码实现如下所示:
class IntSetBST2 { private: int n, *v, vn; struct node { int val; node *left, *right; }; node *root, *freenode, *sentinel; void traverse(node *p) { if (p == sentinel) return; traverse(p->left); v[vn++] = p->val; traverse(p->right); } public: IntSetBST2(int maxelements, int maxval) { root = sentinel = new node; n = 0; freenode = new node[maxelements]; } int size() { return n; } void insert(int t) { sentinel->val = t; node **p = &root; //指向指针的指针 while ((*p)->val != t) if (t < (*p)->val) p = &((*p)->left); else p = &((*p)->right); if (*p == sentinel) { *p = freenode++; (*p)->val = t; (*p)->left = (*p)->right = sentinel; n++; } } void report(int *x) { v = x; vn = 0; traverse(root); } };
(4)说明如何通过使用低开销的逻辑移位替代高开销的除法运算来对箱进行加速。
为了用移位取代除法,通过类似下面的伪代码对变量进行初始化:
goal = n/m; binshift = 1; for(int i=2;i<goal;i*=2) binshift++; nbins = 1 + (n >> binshift);
插入函数从该结点开始:
p = &(bin[t >> binshift]);