MIT:算法导论——7.1.基本数据结构_栈、队列、链表、有根树
2014-06-12 10:30
453 查看
【教材:第10章 基本数据结构】
(1)栈——后进先出
说明:采用一个数组S[1...n]来实现一个最多容纳n个元素的栈。
S.top=0时,栈不包含任何元素,栈空。下溢,上溢:S.top超过n。
栈的几种操作:
(2)队列——先进后出
说明:采用一个数组Q[1...n]来实现一个最多容纳n个元素的队列。
元素存放在Q.head,Q.head+1, ..., Q.tail - 1。
当Q.head = Q.tail时,队列为空;初始时Q.head = Q.tail = 1。
当Q.head = Q.tail + 1时,队列为满。
队列的几种操作:
//Q.tail处不存放内容,Q.head为第一个元素。
【双端队列】插入和删除操作都可以在两端进行,四个时间均为O(1)的过程,用数组实现。
(3)链表(linked list)
【双向链表】(doublely linked list),
其每个元素是一个对象,每个对象有一个关键字key和两个指针:next和pre。
x.pre = NULL,没有前驱,为链表头;x.next = NULL,没有后继,为链表尾。L.head = NULL,链表空。
【带哨兵的双向链表】设置L.nil对象,链表转为有哨兵的双向循环链表,使得NULL都替换为L.nil
// 初始时L.nil.next = L.nil, L.nil.pre = L.nil。
【循环链表】表头元素的pre指针指向表尾元素,表尾元素的next指针指向表头元素。
(4)有根树的表示
本节讨论与链式数据结构表示有根树的问题。
【二叉树】
结点元素,p:父结点,left:左孩子,right:右孩子。
x.p = NULL,则x是根结点;x.left = NULL,x没有左孩子;x.right类似。
T.root指向整棵树的根结点,T.root = NULL,则该树为空。
【分支无限制的有根树】
孩子分支固定为k的,可以用child1,child2, ...,childk表示。
当孩子数任意时,可以用左孩子右兄弟表示法(left-child, right-sibling representation)。
每个结点包括一个父结点指针p,以及:
(1)x.left-child指向结点x最左边的孩子结点。
(2)x.right-sibling指向x右侧相邻的兄弟结点。
【树的其他表示法】
例如:完全二叉树使用堆来表示,堆用一个单数组加上堆的最末结点的下标表示。
哪种方法最优取决于具体应用
=====================具体C++代码实现==============================================
不带哨兵的双向链表的模板实现:功能属性标准STL List
【教训】对于模板类、模板函数、模板重载操作符,用友元授权时,都要用如下格式:名称<T>。
测试程序——
待写。。。
=====================博客补充======================================================
1、栈和队列
栈和队列都是动态集合,元素的出入是规定好的。栈规定元素是先进后出(FILO),队列规定元素是先进先出(FIFO)。栈和队列的实现可以采用数组和链表进行实现。在标准模块库STL中有具体的应用,可以参考http://www.cplusplus.com/reference/。
栈的基本操作包括入栈push和出栈pop,栈有一个栈顶指针top,指向最新如栈的元素,入栈和出栈操作操作都是从栈顶端进行的。
队列的基本操作包括入队enqueue和出队dequeue,队列有队头head和队尾tail指针。元素总是从队头出,从队尾入。采用数组实现队列时候,为了合理利用空间,可以采用循环实现队列空间的有效利用。
关于栈和队列的基本操作如下图所示:
采用数组简单实现一下栈和队列,实现队列时候,长度为n的数组最多可以含有n-1个元素,循环利用,这样方便判断队列是空还是满。
问题:
(1)说明如何用两个栈实现一个队列,并分析有关队列操作的运行时间。
解答:栈中的元素是先进后出,而队列中的元素是先进先出。现有栈s1和s2,s1中存放队列中的结果,s2辅助转换s1为队列。入队列操操作:当一个元素入队列时,先判断s1是否为空,如果为空则新元素直接入s1,如果非空则将s1中所有元素出栈并存放到s2中,然后在将元素入s1中,最后将s2中所有元素出栈并入s1中。此时s1中存放的即是队列入队的顺序。出队操作:如果s1为空,则说明队列为空,非空则s1出栈即可。入队过程需要在s1和s2来回交换,运行时间为O(n),出队操作直接是s1出栈运行时间为O(1)。举例说明转换过程,如下图示:
(2)说明如何用两个队列实现一个栈,并分析有关栈操作的运行时间。
解答:类似上面的题目,队列是先进先出,而栈是先进后出。现有队列q1和q2,q1中存放的是栈的结果,q2辅助q1转换为栈。入栈操作:当一个元素如栈时,先判断q1是否为空,如果为空则该元素之间入队列q1,如果非空则将q1中的所有元素出队并入到q2中,然后将该元素入q1中,最后将q2中所有元素出队并入q1中。此时q1中存放的就是栈的如栈顺序。出栈操作:如果q1为空,则栈为空,否则直接q1出队操作。入栈操作需要在队列q1和q2直接来来回交换,运行时间为O(n),出栈操作是队列q1出队操作,运行时间为O(1)。我用C++语言实现完整程序如下:
2、链表
链表与数组的区别是链表中的元素顺序是有各对象中的指针决定的,相邻元素之间在物理内存上不一定相邻。采用链表可以灵活地表示动态集合。链表有单链表和双链表及循环链表。书中着重介绍了双链表的概念及操作,双链表L的每一个元素是一个对象,每个对象包含一个关键字和两个指针:next和prev。链表的操作包括插入一个节点、删除一个节点和查找一个节点,重点来说一下双向链表的插入和删除节点操作,图例如下:
链表是最基本的数据结构,凡是学计算机的必须的掌握的,在面试的时候经常被问到,关于链表的实现,百度一下就知道了。在此可以讨论一下与链表相关的练习题。
(1)在单链表上插入一个元素,要求时间复杂度为O(1)。
解答:一般情况在链表中插入一元素是在末尾插入的,这样需要从头遍历一次链表,找到末尾,时间为O(n)。要在O(1)时间插入一个新节点,可以考虑每次在头节点后面插入,即每次插入的节点成为链表的第一个节点。
(2)在单链表上删除一个给定的节点p,要求时间复杂度为O(1)。
解答:一般情况删除一个节点时候,我们需要找到该节点p的前驱节点q,需要对链表进行遍历,运行时间为O(n-1)。我们可以考虑先将q的后继节点s的值替换q节点值,然后删除s即可。如下图删除节点q的操作过程:
(3)单链表逆置,不允许额外分配存储空间,不允许递归,可以使用临时变量,执行时间为O(n)。
解答:这个题目在面试笔试中经常碰到,基本思想上将指针逆置。如下图所示:
(4)遍历单链表一次,找出链表中间节点。
解答:定义两个指针p和q,初始都指向链表头节点。然后开始向后遍历,p每次移动2步,q移动一步,当p到达末尾的时候,p正好到达了中间位置。
(5)用一个单链表L实现一个栈,要求push和pop的操作时间为O(1)。
解答:根据栈中元素先进后出的特点,可以在链表的头部进行插入和删除操作。
(6)用一个单链表L实现一个队列,要求enqueue和dequeue的操作时间为O(1)。
解答:队列中的元素是先进先出,在单链表结构中增加一个尾指针,数据从尾部插入,从头部删除。
3、有根树的表示
采用链表数据结构来表示树,书中先降二叉树的链表表示法,然后拓展到分支数无限制的有根数。先来看看二叉树的链表表示方法,用域p、left和right来存储指向二叉树T中的父亲、左孩子和右孩子的指针。如下图所示:
对于分支数目无限制的有根树,采用左孩子、右兄弟的表示方法。这样表示的树的每个节点都包含有一个父亲指针p,另外两个指针:
(1)left_child指向节点的最左孩子。
(2)right_sibling指向节点紧右边的兄弟。
(1)栈——后进先出
说明:采用一个数组S[1...n]来实现一个最多容纳n个元素的栈。
S.top=0时,栈不包含任何元素,栈空。下溢,上溢:S.top超过n。
栈的几种操作:
STACK-EMPTY( S ) if S.top == 0 return true else return false PUSH( S, x )// 数组尾部,栈顶入栈 if S.top == n error "overflow" else S.top += 1 S[S.top] = x POP( S )// 数组尾部,栈顶出栈 if STACK-EMPTY( S ) error "underflow" else S.top -= 1 return S[S.top + 1];
(2)队列——先进后出
说明:采用一个数组Q[1...n]来实现一个最多容纳n个元素的队列。
元素存放在Q.head,Q.head+1, ..., Q.tail - 1。
当Q.head = Q.tail时,队列为空;初始时Q.head = Q.tail = 1。
当Q.head = Q.tail + 1时,队列为满。
队列的几种操作:
//Q.tail处不存放内容,Q.head为第一个元素。
ENQUEUE( Q, x )// 队尾进队 if ( Q.tail + 1 ) % Q.length == Q.head error "overflow" else Q[Q.tail] = x Q.tail = ( Q.tail + 1 ) % Q.length DEQUEUE( Q )// 队首出队 if Q.head == Q.tail error "underflow" else x = Q[Q.head] Q.head = ( Q.head + 1 ) % Q.length return x
【双端队列】插入和删除操作都可以在两端进行,四个时间均为O(1)的过程,用数组实现。
(3)链表(linked list)
【双向链表】(doublely linked list),
其每个元素是一个对象,每个对象有一个关键字key和两个指针:next和pre。
x.pre = NULL,没有前驱,为链表头;x.next = NULL,没有后继,为链表尾。L.head = NULL,链表空。
(1)链表搜索 LIST-SEARCH( L, k ) x = L.head while x != NULL && x.key != k x = x.next return x// 经典&犀利呀:2014-6-12 10:04:18 (2)链表插入 LIST-INSERT( L, x )// x是一个关键字为k的元素,将其插入链表前端 x.next = L.head x.pre = NULL if L.head != NULL L.head.pre = x L.head = x (3)链表删除 LIST-DELETE( L, x ) if L.head == x L.head = x.next else x.pre.next = x.next if x.next != NULL x.next.pre = x.pre
【带哨兵的双向链表】设置L.nil对象,链表转为有哨兵的双向循环链表,使得NULL都替换为L.nil
// 初始时L.nil.next = L.nil, L.nil.pre = L.nil。
(1)链表搜索 LIST-SEARCH'( L, k ) x = L.head while x != L.nil && x.key != k x = x.next return x// 经典&犀利呀:2014-6-12 10:04:18 (2)链表插入 LIST-INSERT'( L, x )// x是一个关键字为k的元素,将其插入链表前端 x.next = L.nil.next x.pre = L.nil L.nil.next.pre = x L.nil.next = x (3)链表删除 LIST-DELETE'( L, x ) x.pre.next = x.next x.next.pre = x.pre
【循环链表】表头元素的pre指针指向表尾元素,表尾元素的next指针指向表头元素。
(4)有根树的表示
本节讨论与链式数据结构表示有根树的问题。
【二叉树】
结点元素,p:父结点,left:左孩子,right:右孩子。
x.p = NULL,则x是根结点;x.left = NULL,x没有左孩子;x.right类似。
T.root指向整棵树的根结点,T.root = NULL,则该树为空。
【分支无限制的有根树】
孩子分支固定为k的,可以用child1,child2, ...,childk表示。
当孩子数任意时,可以用左孩子右兄弟表示法(left-child, right-sibling representation)。
每个结点包括一个父结点指针p,以及:
(1)x.left-child指向结点x最左边的孩子结点。
(2)x.right-sibling指向x右侧相邻的兄弟结点。
【树的其他表示法】
例如:完全二叉树使用堆来表示,堆用一个单数组加上堆的最末结点的下标表示。
哪种方法最优取决于具体应用
=====================具体C++代码实现==============================================
不带哨兵的双向链表的模板实现:功能属性标准STL List
// 为保证唯一性,头文件的命名应基亍其所在项目源代码树的全路径。 // 例如,项目foo中的头文件foo/src/bar/baz.h挄如下方式保护: #ifndef FOO_BAR_BAZ_H_ #define FOO_BAR_BAZ_H_ #endif // FOO_BAR_BAZ_H_ #ifndef ALGORITHM_LIST_H_ #define ALGORITHM_LIST_H_ #include <cstdio> #include <iostream> #include <string> #include <stdexcept> using namespace std; #define BETTER template<class T> class List; #ifdef BETTER template<class T> ostream& operator<<( ostream&, const List<T>& ); #endif template<class T> class Node{ friend List<T>; #ifdef BETTER friend ostream& operator<< <T>( ostream&, const List<T>& ); #endif public: Node( const T& tIn = 0 ) : data( tIn ){} private: T data; Node<T> *pre, *next; }; // 其每个元素是一个对象,每个对象有一个关键字key和两个指针:next和pre。 // x.pre = NULL,没有前驱,为链表头; // x.next = NULL,没有后继,为链表尾。 // L.head = NULL,链表空。 template<class T>// 插入、删除的都是T值 class List{ #ifdef BETTER friend ostream& operator<< <T>( ostream&, const List<T>& ); #endif public: List(void){ head = NULL; }// NULL = 0 ~List(void); Node<T>* Search( T& k ) const; List<T>& Insert( Node<T>* x );// 插入在头部 List<T>& Delete( Node<T>* x ); List<T>& Insert( int k, const T& x);// k[0...] List<T>& Delete( int k, T& x);// k[1...] void Output( ostream& ) const; private: Node<T> *head; }; template<class T> List<T>::~List(void) { Node<T> *nextNode; while( head != NULL ){ // 异常安全性,先不要改变head nextNode = head->next; delete head; head = nextNode; } } template<class T> Node<T>* List<T>::Search( T& k ) const { Node<T> *x; while( x != NULL && x->data != k ) x = x->next; return x; } // x是一个关键字为k的元素,将其插入链表前端 template<class T> List<T>& List<T>::Insert( Node<T>* x ) {// 插入在头部 x->next = head; x->pre = NULL; if( head != NULL ) head->pre = x; head = x; return *this; } template<class T> List<T>& List<T>::Delete( Node<T>* x ) { if( head == x ) head = x->next; else x->pre->next = x->next; if( x->next != NULL ) x->next->pre = x->pre; return *this; } template<class T> List<T>& List<T>::Insert( int k, const T& x) {// 在第k个元素之后,插入x。有效位置[1...],故x=0代表在链表首部插入 if( k < 0 ) throw out_of_range( "不存在第k个元素" ); int index = 1; Node<T> *cur = head; while( index < k && cur ){ cur = cur->next; ++index; } //if( index != k )// 因为k可以等于0,此时不等于index if( k > 0 && cur == NULL )// 链表本身可能为空 throw out_of_range( "不存在第k个元素" ); Node<T> *nodeIn = new Node<T>; nodeIn->data = x; if( k != 0 ){ nodeIn->next = cur->next; nodeIn->pre = cur; cur->next = nodeIn; }else{ nodeIn->next = head; nodeIn->pre = NULL; head = nodeIn; } if( nodeIn->next != NULL ) nodeIn->next->pre = nodeIn; return *this; } template<class T> List<T>& List<T>::Delete( int k, T& x) { if( k < 1 ) throw out_of_range( "不存在第k个元素" ); Node<T> *cur = head, *pre = NULL; int index = 1; //while( index < k && cur ){ // cur = cur->next; // ++index; //} //if( cur == NULL ) // throw out_of_range( "不存在第k个元素" ); // 可以只用一个指针来完成 pre = head; while( index < k - 1 && pre ){ pre = pre->next; ++index; } if( pre == NULL || pre->next == NULL ) throw out_of_range( "不存在第k个元素" ); cur = pre->next; pre->next = cur->next; x = cur->data; delete cur; return *this; } template<class T> void List<T>::Output( ostream& out ) const { Node<T> *cur = head; while( cur != NULL ){ out << cur->data << "\t"; cur = cur->next; } } #ifdef BETTER template<class T> ostream& operator<<( ostream& out, const List<T>& L ) { Node<T> *cur = L.head; while( cur != NULL ){ out << cur->data << "\t"; cur = cur->next; } return out; } #elif 0 template<class T> ostream& operator<<( ostream& out, const List<T>& L ) { L.Output( out ); return out; } #endif #endif // ALGORITHM_LIST_H_ #if 0 对了,因为模板没法实现 声明和定义分离. 所以有使用到模板的定义都必须放到.h里面.. #endif
【教训】对于模板类、模板函数、模板重载操作符,用友元授权时,都要用如下格式:名称<T>。
测试程序——
#include "List.h" #include <iostream> using namespace std; int main( void ) { cout << "2014-6-14 15:23:17" << endl; List<int> L; Node<int> *pTmp = NULL; int i; for( i = 0; i < 10; ++i ){ Node<int> *x = new Node<int>( i * 2 ); pTmp = x; //x->data = i * 2; L.Insert( x );// 插入在头部 } cout << L << endl; int x; L.Delete( pTmp );// 删除最后插入的结点,在头部 L.Delete( --i, x );// 删除尾部的结点 cout << L << endl; L.Insert( --i, x );// 将删除的结点再插入尾部 cout << L << endl; L.Insert( 0, x ); // 将删除的结点插入头部 cout << L << endl; // List<int> L; return 0; }测试结果——
待写。。。
=====================博客补充======================================================
1、栈和队列
栈和队列都是动态集合,元素的出入是规定好的。栈规定元素是先进后出(FILO),队列规定元素是先进先出(FIFO)。栈和队列的实现可以采用数组和链表进行实现。在标准模块库STL中有具体的应用,可以参考http://www.cplusplus.com/reference/。
栈的基本操作包括入栈push和出栈pop,栈有一个栈顶指针top,指向最新如栈的元素,入栈和出栈操作操作都是从栈顶端进行的。
队列的基本操作包括入队enqueue和出队dequeue,队列有队头head和队尾tail指针。元素总是从队头出,从队尾入。采用数组实现队列时候,为了合理利用空间,可以采用循环实现队列空间的有效利用。
关于栈和队列的基本操作如下图所示:
采用数组简单实现一下栈和队列,实现队列时候,长度为n的数组最多可以含有n-1个元素,循环利用,这样方便判断队列是空还是满。
问题:
(1)说明如何用两个栈实现一个队列,并分析有关队列操作的运行时间。
解答:栈中的元素是先进后出,而队列中的元素是先进先出。现有栈s1和s2,s1中存放队列中的结果,s2辅助转换s1为队列。入队列操操作:当一个元素入队列时,先判断s1是否为空,如果为空则新元素直接入s1,如果非空则将s1中所有元素出栈并存放到s2中,然后在将元素入s1中,最后将s2中所有元素出栈并入s1中。此时s1中存放的即是队列入队的顺序。出队操作:如果s1为空,则说明队列为空,非空则s1出栈即可。入队过程需要在s1和s2来回交换,运行时间为O(n),出队操作直接是s1出栈运行时间为O(1)。举例说明转换过程,如下图示:
(2)说明如何用两个队列实现一个栈,并分析有关栈操作的运行时间。
解答:类似上面的题目,队列是先进先出,而栈是先进后出。现有队列q1和q2,q1中存放的是栈的结果,q2辅助q1转换为栈。入栈操作:当一个元素如栈时,先判断q1是否为空,如果为空则该元素之间入队列q1,如果非空则将q1中的所有元素出队并入到q2中,然后将该元素入q1中,最后将q2中所有元素出队并入q1中。此时q1中存放的就是栈的如栈顺序。出栈操作:如果q1为空,则栈为空,否则直接q1出队操作。入栈操作需要在队列q1和q2直接来来回交换,运行时间为O(n),出栈操作是队列q1出队操作,运行时间为O(1)。我用C++语言实现完整程序如下:
2、链表
链表与数组的区别是链表中的元素顺序是有各对象中的指针决定的,相邻元素之间在物理内存上不一定相邻。采用链表可以灵活地表示动态集合。链表有单链表和双链表及循环链表。书中着重介绍了双链表的概念及操作,双链表L的每一个元素是一个对象,每个对象包含一个关键字和两个指针:next和prev。链表的操作包括插入一个节点、删除一个节点和查找一个节点,重点来说一下双向链表的插入和删除节点操作,图例如下:
链表是最基本的数据结构,凡是学计算机的必须的掌握的,在面试的时候经常被问到,关于链表的实现,百度一下就知道了。在此可以讨论一下与链表相关的练习题。
(1)在单链表上插入一个元素,要求时间复杂度为O(1)。
解答:一般情况在链表中插入一元素是在末尾插入的,这样需要从头遍历一次链表,找到末尾,时间为O(n)。要在O(1)时间插入一个新节点,可以考虑每次在头节点后面插入,即每次插入的节点成为链表的第一个节点。
(2)在单链表上删除一个给定的节点p,要求时间复杂度为O(1)。
解答:一般情况删除一个节点时候,我们需要找到该节点p的前驱节点q,需要对链表进行遍历,运行时间为O(n-1)。我们可以考虑先将q的后继节点s的值替换q节点值,然后删除s即可。如下图删除节点q的操作过程:
(3)单链表逆置,不允许额外分配存储空间,不允许递归,可以使用临时变量,执行时间为O(n)。
解答:这个题目在面试笔试中经常碰到,基本思想上将指针逆置。如下图所示:
(4)遍历单链表一次,找出链表中间节点。
解答:定义两个指针p和q,初始都指向链表头节点。然后开始向后遍历,p每次移动2步,q移动一步,当p到达末尾的时候,p正好到达了中间位置。
(5)用一个单链表L实现一个栈,要求push和pop的操作时间为O(1)。
解答:根据栈中元素先进后出的特点,可以在链表的头部进行插入和删除操作。
(6)用一个单链表L实现一个队列,要求enqueue和dequeue的操作时间为O(1)。
解答:队列中的元素是先进先出,在单链表结构中增加一个尾指针,数据从尾部插入,从头部删除。
3、有根树的表示
采用链表数据结构来表示树,书中先降二叉树的链表表示法,然后拓展到分支数无限制的有根数。先来看看二叉树的链表表示方法,用域p、left和right来存储指向二叉树T中的父亲、左孩子和右孩子的指针。如下图所示:
对于分支数目无限制的有根树,采用左孩子、右兄弟的表示方法。这样表示的树的每个节点都包含有一个父亲指针p,另外两个指针:
(1)left_child指向节点的最左孩子。
(2)right_sibling指向节点紧右边的兄弟。
相关文章推荐
- 算法导论 第三部分——基本数据结构——栈、队列、链表、散列表
- 链表_第10章_基本数据结构_算法导论
- 栈与队列_第10章_基本数据结构_算法导论
- 算法导论——第七章——基本数据结构
- 基本数据结构(算法导论)与python
- 算法导论第十章 基本数据结构实现(栈,队列,链表),课后题答案
- 算法导论10(基本数据结构)
- C++类模板 实现双向循环链表的基本算法 《数据结构》(C++版 北京科海)中摘抄
- 算法导论笔记:10基本数据结构(番外)
- 【算法导论】链表队列
- 算法导论第10章基本数据结构10.1栈
- 数据结构类型定义及基本操作汇总(一)--线性表,单链表,栈和队列
- 数据结构基本算法:图的存储(以邻接链表为例)
- 【数据结构和算法分析】单链表的基本实现
- 基本数据结构(算法导论)与python
- 多核计算与程序设计 - 06 基本算法和数据结构 之三 哈希表与哈希链表
- 数据结构基本算法:单向链表
- 【数据结构与算法】基本数据结构——队列的链式表示
- c++基本数据结构的类的用法--栈,队列,链表