插入查找元素效率问题——《编程珠玑》读书笔记

        这两天看了第13章,看了好长一段时间,主要花在理解和编程实现上面,感觉自己的理解能力还有待提高。

        这一章主要讲如何实现一个有序集合(Set),该集合插入元素时不能插入重复元素,每次插入完后集合中元素的排列是有序的。书上一共使用了6种数据结构实现这个集合:STL中的set(红黑数)、数组、链表、二分查找树、位向量、桶,使用了3种优化方案:哨兵(标记元素)、指针的指针(用于实现递归向迭代的转换)、块内存分配(一次性分配一大块内存,使用时从分配好的空间中取,不用每次用的时候new一次)。

        下面就二分查找树的方法来举例说明如何使用这3种优化方法:

        首先,实现一个未优化的方案,使用递归实现,没有使用哨兵,每次都比较一下是否到达链表尾,没有使用块内存分配,每次插入一个节点的时候分配一次内存,C++实现如下:

class IntSetBSTRecurse
{
private:
	struct node
	{
		int val;
		node *left, *right;
		node (int t)
		{
			val = t;
			left = right = 0;
		}
	};
	int n, vn, *v;
	node *root;
public:
	IntSetBSTRecurse(int maxelements, int maxval) : n(0), root(0) { }

	int size()
	{
		return n;
	}

	void insert(int t)
	{
		root = rinsert(root, t);
	}

	void report(int *x)
	{
		v = x;
		vn = 0;
		traverse(root);
	}

	~IntSetBSTRecurse()
	{
		deletenode(root);
	}
private:
	node* rinsert(node* p, int t)
	{
		if (!p)
		{
			p = new node(t);
			++n;
		}
		else if (p->val < t)
			p->right = rinsert(p->right, t);
		else if (p->val > t)
			p->left = rinsert(p->left, t);

		return p;
	}

	void traverse(node* p)
	{
		if (p)
		{
			traverse(p->left);
			v[vn++] = p->val;
			traverse(p->right);
		}
	}

	void deletenode(node* p)
	{
		if(p)
		{
			deletenode(p->left);
			deletenode(p->right);
			delete p;
			p = 0;
		}
	}
};

        然后把插入元素的函数改为迭代实现,暂不使用哨兵、块内存分配、指针的指针等优化方案,实现如下:

	void insert(int t)
	{
		if (!root)
		{
			root = new node(t);
			++n;
			return;
		}
		node* p = root;
		while (p)
		{
			if (t < p->val)
			{
				if (p->left)
					p = p->left;
				else
				{
					p->left = new node(t);
					++n;
					return;
				}
			}
			else if (t > p->val)
			{
				if (p->right)
					p = p->right;
				else
				{
					p->right = new node(t);
					++n;
					return;
				}
			}
			else
				return;
		}
	}

        可以看出,改成迭代的方式要考虑很多情况,代码比较复杂,可以使用指针的指针简化这种复杂性,不过这个有点难理解,需要画画图才能更好的理解(我比较笨,一眼没法看透)。

        再使用上面3种优化方案,实现如下:

class IntSetBSTIterate
{
private:
	struct node
	{
		int val;
		node *left, *right;
		node()
		{
			val = 0;
			left = right = 0;
		}
		node(int t)
		{
			val = t;
			left = right = 0;
		}
		node(int t, node* s)
		{
			val = t;
			left = right = s;
		}
	};

	node *root, *sentinel, *freenode, *pnewnode;
	int n, *v, vn;
public:
	IntSetBSTIterate(int maxelements, int maxval) : n(0), v(0), vn(0)
	{
		root = sentinel = new node(maxval, 0);
		pnewnode = new node[maxelements];
		freenode = pnewnode;
	}

	int size()
	{
		return n;
	}

	void insert(int t)
	{
		sentinel->val = t; 
		node **p = &root;	// pointer to pointer
		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)
	{
		node* p = root;
		v = x;
		vn = 0;
		traverse(p);
	}

	~IntSetBSTIterate()
	{
		delete sentinel;
		root = sentinel = 0;
		delete[] pnewnode;
		pnewnode = freenode = 0;
	}
private:
	void traverse(node* p)
	{
		if (p != sentinel)
		{
			traverse(p->left);
			v[vn++] = p->val;
			traverse(p->right);
		}
	}
};

        上面使用freenode进行预分配内存,一次性把所有的内存都分配好,在插入时要用的内存时从已经分配好的内存块中取出使用;同时使用哨兵sentinel,让每个叶节点都指向哨兵,不用每次都担心指针是否指向最后的空元素,哨兵可以减少比较的次数;最后一个优化是使用指向指针的指针实现迭代,用法很巧妙,p指向的是节点的next指向的地址,改变*p的值就是改变next的指向,巧妙的插入一个节点,同时改变整个链表的指向。

阅读更多
换一批

没有更多推荐了,返回首页