算法导论之九(10.2不带哨兵节点和带哨兵节点的双向链表)

不带哨兵节点的双向链表即一般的双向链表,有一个头指针指向第一个节点,每个节点有key值和两个指针next和pre,分别指向前后相邻的节点,头结点的pre=NULL,尾节点的next=NULL,比较明了,但是也有麻烦的地方:在做查找删除节点等操作的时候,免不了要判断边界条件,比如node==NULL等。先来看看这种链表的代码:

/*
 * 实现没有哨兵节点的双向链表,需要自己判断边界条件
 */
#include <iostream>

using namespace std;

class list {
	struct node {
		int key;
		node* next;
		node* pre;
		node(int k) :
				key(k), next(NULL), pre(NULL) {
		}
	};

public:
	node* head;
	int len;

	list() :
			head(NULL), len(0) {
	}

	~list() {
		node* tmp1 = head, *tmp2;

		while (tmp1 != NULL) {
			tmp2 = tmp1->next;
			delete tmp1;
			len--;
			cout << "len=" << len << endl;
			tmp1 = tmp2;
		}
	}

	/*
	 * 在头处插入节点
	 */
	void insertHead(int k) {
		node* n = new node(k);
		n->next = head;
		head = n;
		if (n->next != NULL) {
			n->next->pre = n;
		}
		len++;
	}

	/*
	 * 删除指针指向的节点
	 */
	int del(node* n) {
		if (n == NULL) {
			return -1;
		}
		if (n->pre != NULL) {
			n->pre->next = n->next;
		} else {
			head = n->next;
		}
		if (n->next != NULL) {
			n->next->pre = n->pre;
		}

		delete n;
		len--;
		return n->key;
	}

	/*
	 * 查找具有某个key值的节点
	 */
	node* searchList(int k) {
		if (len == 0) {
			return NULL;
		}
		node* tmp = head;
		while (tmp != NULL) {
			if (tmp->key == k) {
				break;
			}
			tmp = tmp->next;
		}
		return tmp;
	}

	/*
	 * 遍历list
	 */
	void travelList() {
		node* tmp = head;
		while (tmp != NULL) {
			cout << tmp->key << ' ';
			tmp = tmp->next;
		}
		cout << endl;
	}

};

int main() {

	list* l = new list();
	l->insertHead(5);
	l->insertHead(4);
	l->insertHead(3);
	l->travelList();
	l->del(l->head->next);
	l->travelList();

	delete l;

	return 0;
}

每次判断边界条件,虽然不会从根本上增加时间复杂度,但是对其常数项还是有影响的;而如果使用带哨兵节点构成的双向循环链表,则可以省去这些问题。我们使用一个“哑的”NIL节点来代替之前的head头指针,NIL节点的key值没有实际的意义,主要关注它的next和pre,初始的时候,链表只有一个NIL节点,NIL.next指向自己,NIL.pre也指向自己。当添加了若干个节点之后,NIL.next指向头节点,而NIL.pre则指向尾节点;而同样的,这时头节点的pre不再是NULL而是指向NIL,尾节点的next也不再是NULL,也是指向NIL。

这样的好处在于,我们判断边界条件的时候,不需要再判断是否为空,尤其在删除节点的时候,只需要写两句即可。但是这样也带来一些问题,就是要额外分配空间来存储NIL节点,如果对于多个比较短的链表而言,这样可能会代码比较大的冗余空间。

代码如下:

/*
 * 实现带哨兵是双向循环链表
 */
#include <iostream>

using namespace std;

class list {
	struct node {
		int key;
		node* next;
		node* pre;
		node(int k) :
				key(k), next(NULL), pre(NULL) {
		}
	};

//	node* head;
public:
	node* nil; //哨兵节点
	int len;
	list() :
			nil(), len(0) {
		nil = new node(0); //初始化哨兵节点
		//让链表循环起来
		nil->next = nil;
		nil->pre = nil;
	}

	~list() {
		//析构的时候要delete掉还存在于list中的节点
		if (len == 0) {
			delete nil;
			return;
		}
		node* n1 = nil->next;
		node* n2 = NULL;
		while (n1 != NULL && n1 != nil) {
			n2 = n1->next;
			delete n1;
			len--;
			n1 = n2;
		}
		delete nil;
	}

	/*
	 * 在头部插入节点
	 */
	void insertHead(int k) {
		node* n = new node(k);
		n->next = nil->next;
		n->pre = nil;

		nil->next = n;

		//这一句不要丢了
		n->next->pre = n;
		len++;
	}

	/*
	 * 删除节点,这里删除的操作只需要写两句,比不带哨兵的链表操作要简洁的多
	 */
	void del(node* n) {
		n->pre->next = n->next;
		n->next->pre = n->pre;
		delete n;
		len--;
	}

	node* searchList(int k) {
		node* tmp = nil->next;

		//让nil的key值永远不可能等于k
//		nil->key = k + 1;

//		while (tmp->key != k) {
		while (tmp != nil && tmp->key != k) {
			tmp = tmp->next;
		}
		if (tmp != nil) {
			return tmp;
		} else {
			return NULL;
		}
	}

	/*
	 * 从next指针方向遍历链表
	 */
	void travelClockwise() {
		node* tmp = nil->next;
		while (tmp != nil) {
			cout << tmp->key << ' ';
			tmp = tmp->next;
		}
		cout << endl;
	}

	/*
	 * 从pre指针方向遍历链表
	 */
	void travelAnticlockwise() {
		node* tmp = nil->pre;
		while (tmp != nil) {
			cout << tmp->key << ' ';
			tmp = tmp->pre;
		}
		cout << endl;

	}

};

int main() {

	list* l = new list();
	l->insertHead(5);
	l->insertHead(4);
	l->insertHead(3);
	l->travelClockwise();
	l->del(l->nil->pre);
//	l->travelClockwise();
	l->travelAnticlockwise();
	return 0;
}
时间: 2024-10-09 00:13:44

算法导论之九(10.2不带哨兵节点和带哨兵节点的双向链表)的相关文章

算法导论之八(10.1-5单数组实现双端队列)

算法导论第三版P131 题目: 10.1-5 栈插入和删除元素只能在同一端进行,队列的插入操作和删除操作分别在两端进行,与它们不同的,有一种双端队列(deque),其插入和删除操作都可以在两端进行.写出4个时间均为O(1)的过程,分别实现在双端队列插入和删除元素的操作,该队列使用一个数组实现的. 注意点: 1.左右端点指向的位置是类似于队列中的tail端点,是下一个插入操作的位置. 2.然后注意遍历的时候,左端点和右端点的位置关系,有两种可能,所以遍历的方式不一样. 代码: /* * 使用单数组

算法导论第十九章 斐波那契堆

<算法导论>第二版中在讨论斐波那契堆之前还讨论了二项堆,但是第三版中已经把这块的内容放到思考题中,究极原因我想大概是二项堆只是个引子,目的是为了引出斐波那契堆,便于理解,而且许多经典的算法实现都是基于斐波那契堆,譬如计算最小生成树问题和寻找单源最短路径问题等,此时再把二项堆单独作为一章来讲显然没有必要.类似的堆结构还有很多,如左倾堆,斜堆,二项堆等,下次我打算开一篇博客来记录下它们的异同点. 一.摊还分析(第十七章) 这些高级的数据结构的性能分析一般是基于一个技术——摊还分析,可以理解成一种时

选择问题&mdash;&mdash;算法导论(10)

1. 引言     这一篇我们来探讨选择问题. 它的提法是: 输入:一个包含n个(互异)数的序列A和一个数i(1≤i≤n). 输出:元素x(x∈A),且A中有i-1个元素比x小. 简单的说,就是在A中找到第i小的数. 2. 期望为线性时间的选择算法 (1) 算法描述与实现     我们先给出算法的伪代码描述: 其主要思想与我们前面介绍的快速排序--算法导论(8)基本一样,只是在该问题中,我们每次递归时,只用考虑第i小的数可能出现的分组. 下面给出Java实现代码: public static v

算法导论-第24章 Dijkstra算法

Dikstra算法解决的是有向图上单源最短路径问题(无向图可以看成有相反的两条有向边),且要求边的权重都是非负值. 算法导论用了很多引理,性质来证明Dijstra算法的正确性,这里不说了,也表达不明白,只说我理解的过程. 有一个图G( V,E) ,选定一个源点s,维护一个集合Q=V-s,  Q中点有一个d值表示此时从s到该点的已知距离,s.d=0 :初始化都为正无穷,表明不可达.然后对s点所连接的点(设为点集M)进行松弛操作,就是设点m属于M, m.d > s.d+ w(s,m) 则更新 m.d

《算法导论》中动态规划求解钢条切割问题

动态规划算法概述 动态规划(dynamic programming)1是一种与分治方法很像的方法,都是通过组合子问题的解来求解原问题.不同之处在于,动态规划用于子问题重叠的情况,比如我们学过的斐波那契数列.在斐波那契数列的求解问题中,我们经常要对一个公共子问题进行多次求解,而动态规划算法,则对每个子问题只求解一次,将其解保存在一个表格中,从而避免了大量的冗余计算量. 动态规划算法常用于寻找最优解问题(optimization problem).而其规划大概可分为四步: 1.刻画一个最优解的结构特

算法导论--动态规划(钢条切割)

钢条切割问题 现有一段长度为n英寸的钢条和一个价格表pi,求切割方案使销售利益最大rn最大 长度为n英寸的钢条共有2n?1种不同的切割方案,因为可以每个整英寸的位置都可以决定切割或者不切割. 为了得到rn最大,可以把这个问题分成子问题求解,先切一刀,再考虑余下的部分的最大收益即求 rn=max{pk+rn?k}(k=1,2,3-n-1), pk部分不进行继续切割,直接作为一个整体售出 ; rn?k部分继续切割,考虑所有的情况,分成子问题. 求出所有k值对应的收益最大者作为rn 也有可能不进行任何

算法导论读书笔记之钢条切割问题

算法导论读书笔记之钢条切割问题 巧若拙(欢迎转载,但请注明出处:http://blog.csdn.net/qiaoruozhuo) 给定一段长度为n英寸的钢条和一个价格表 pi (i=1,2, -,n),求切割钢条的方案,使得销售收益rn最大.注意,如果长度为n英寸的钢条价格pn足够大,最优解可能就是完全不需要切割. 若钢条的长度为i,则钢条的价格为Pi,如何对给定长度的钢条进行切割能得到最大收益? 长度i   1   2    3   4     5      6     7     8  

[算法导论 Ch9 中位数和顺序统计量] Selection in O(n)

1. 寻找第k大(小)的数 假设数据存储在数组a[1..n]中 首先,寻找一个数组中最大或者最小的数,因为最大(小)的数一定要比其他所有的数大(小),因此至少要比较完所有的pair才能确定,所以时间复杂度在O(n).那么寻找第k大(小)呢? 比较直观的,就是对数组中国所有的数据先进行排序,在我们这种渣渣的计算机入门选手而言,可选的有QuickSort,MergeSort和HeapSort,甚至是ShellSort等一些比较高级的方法啊...一般的代价都在O(n*logn)上,然后直接取出即可.

算法导论 红黑树 学习 旋转(二)

学习算法 还是建议看看算法导论 算法导论第三版 如果不看数学推导 仅看伪代码 难度还是适中 本系列只是记录我的学习心得 和伪代码转化代码的过程 深入学习 还是建议大家看看算法书籍 教程更加系统. 本文参考算法导论第13章节 红黑树 代码由本人写成 转载请标明出处 红黑树是一个带颜色的二叉树 有以下5点性能 1 每个节点或者红色或者黑色 2 根节点黑色 3 每个叶子节点(nil)为黑色 4 如果一个节点是红色的则它的两个子节点都是黑色 5 每个节点 该节点到子孙节点的路径上 黑色节点数目相同 如图