插入查找元素效率問題——《編程珠璣》讀書筆記

        這兩天看了第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的指向,巧妙的插入一個節點,同時改變整個鏈表的指向。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章