420 likes | 563 Views
数据结构(二 ). 常宝宝 北京大学计算机科学与技术系 chbb@pku.edu.cn. 内容提要. 线性表的定义 线性表的实现 — 顺序存储结构 线性表的实现 — 链式存储结构 单向链表 双向链表. 线性表. 线性表 n 个结点(数据元素)的有限序列。 始结点唯一,即存在一个结点没有前趋结点。 终结点唯一,即存在一个结点没有后继结点。 除始结点外,其它结点均只有一个前趋结点。 除终结点外,其它结点均只有一个后继结点。 线性表举例 简单数据元素 (6, 17, 28, 50, 92, 188) ( A, B, D, E, C) 复杂数据元素
E N D
数据结构(二) 常宝宝 北京大学计算机科学与技术系 chbb@pku.edu.cn
内容提要 • 线性表的定义 • 线性表的实现 — 顺序存储结构 • 线性表的实现 — 链式存储结构 • 单向链表 • 双向链表
线性表 • 线性表 • n个结点(数据元素)的有限序列。 • 始结点唯一,即存在一个结点没有前趋结点。 • 终结点唯一,即存在一个结点没有后继结点。 • 除始结点外,其它结点均只有一个前趋结点。 • 除终结点外,其它结点均只有一个后继结点。 • 线性表举例 • 简单数据元素 (6, 17, 28, 50, 92, 188) (A, B, D, E, C) • 复杂数据元素 学生成绩登记表
线性表 • 线性表中的各个数据元素具有相同(或相容)的类型。 • 线性表中的元素个数称为线性表的长度。 • 长度为0的线性表称为空表。 • 线性表可视为容纳某类对象的容器。 • 线性表中每个元素有一个表明其位置的编号,始结点编号为0,始结点的直接后继结点编号为1,…,终结点的编号为n-1。(线性表长度为n)
线性表 • 和线性表有关的操作: • 构造一个空表 • 判断线性表是否空表 • 判断线性表是否已满 • 返回线性表的长度 • 将一个线性表清空 • 在线性表的第i个位置插入一个元素 • 删除线性表中第i个元素 • 读取线性表中第i个元素 • 把线性表中的第i个元素替换为另外一个元素 • 顺序遍历线性表中的元素,并对每个元素进行特定的处理
抽象数据类型定义 template <typename list_entry>class List {public: enum error_code { success, range_error, overflow, underflow};protected: ...public: //操作List(); List(const List<list_entry>& copy); ~List(); int size() const; bool full() const; bool empty() const; void clear(); error_code retrieve(int i, list_entry& x) const; error_code replace(int i, const list_entry& x); error_code remove(int i, list_entry& x); error_code insert(int i, const list_entry& x); void traverse( void (*visit)(list_entry&));};
线性表操作 List<list_entry>::List();// PRECONDITION:// POSTCONDITION: 建立了一个空表 void List<list_entry>::clear();// PRECONDITION:// POSTCONDITION: 表中所有元素被删除,变成空表 int List<list_entry>::size() const;// PRECONDITION:// POSTCONDITION: // REMARKS:表中元素个数被返回 bool List<list_entry>::empty() const;// PRECONDITION:// POSTCONDITION: // REMARKS:若线性表是空表,返回true,否则返回flase
线性表操作 error_code List<list_entry>::insert(int i, const list_entry& x);// PRECONDITION: 线性表未满, 0≤i≤n// POSTCONDITION: 线性表的长度增加1,x成为表中的第i个元素,表中原来第i个元// 素及其后继元素位置编号加1// REMARKS:若操作成功,返回success,否则返回错误代码// overflow --- 上溢// range_error --- 插入位置无效 void List<list_entry>::traverse( void (*visit)(list_entry&));// PRECONDITION: // POSTCONDITION: 对线性表中每个元素进行visit规定的操作// REMARKS:
线性表的使用 ...void add(int& x ) { x++; } int main() { List<int> intList; //创建一个元素类型是整数的线性表int x; intList.insert(0, 10); //在线性表中插入一个整数10intList.insert(1, 12); //在线性表中插入一个整数12 if ( intList.empty() ) //判断线性表是否是空表cout << “空表” << endl; else cout << “非空表” << endl; intList.traverse(add); //遍历线性表元素,并给每个元素加1intList.remove(1,x); //删除线性表中第1个元素cout << intList.size() << endl; //显示线性表中的元素个数 ...}
线性表的实现 —顺序存储 • 顺序映象以存储位置先后表示元素间的前趋和后继关系。 • 用一组地址连续的存储单元依次存放线性表中的数据元素。 a0a1…ai-1ai…an-1 线性表的起始地址 称作线性表的基地址
线性表的实现 —顺序存储 • 若每个元素占用 l 个存储单元,用其中第一个单元的地址作为元素的存储位置。则元素 ai 的存储位置可以表示为LOC(ai)。 • 线性表中第 i+1 个元素和第 i个元素的存储位置间满足下列关系:LOC(ai+1) = LOC(ai) + l • 线性表中第 i个元素的存储位置为:LOC(ai) = LOC(a0) + i * l其中LOC(a0)是线性表的基地址。 • C++中,数组元素占据连续存储单元,可用来实现顺序存储的线性表。
线性表的实现 —顺序存储 template <typename list_entry, int max_list=100 >class List {public: enum error_code { success, range_error, overflow, underflow};protected:int count; //线性表中元素个数list_entry entry[max_list]; //线性表存储空间public: //操作List(); List(const List<list_entry>& copy); ~List(); int size() const; bool full() const; bool empty() const; void clear(); error_code retrieve(int i, list_entry& x) const; error_code replace(int i, const list_entry& x); error_code remove(int i, list_entry& x); error_code insert(int i, const list_entry& x); void traverse( void (*visit)(list_entry&));};
线性表的实现 —顺序存储 template <typename list_entry, int max_list >List<list_entry, max_list>::List():count(0) {} template <typename list_entry, int max_list >int List<list_entry, max_list>::size() const { return count;} template <typename list_entry, int max_list >bool List<list_entry, max_list>::full() const { if ( count == max_list ) return true; return false;} template <typename list_entry, int max_list >error_code List<list_entry, max_list>::retrieve(int i, list_entry& x) const { if ( i<0 || i>count-1 ) return range_error; x = entry[i]; return success;}
线性表的实现 —顺序存储 template <typename list_entry, int max_list >error_code List<list_entry, max_list>::insert(int i, list_entry& x) { if ( full() ) return overflow; if ( i<0 || i>count) return range_error; for ( int j=count-1; j>=i; j--) entry[j+1] = entry[j]; entry[i] = x; count++; return success;} • xx template <typename list_entry, int max_list >void List<list_entry, max_list>::traverse( void (*visit)(list_entry&)); { for ( int i=0; i<count; i++) (*visit)(entry[i]);}
线性表的实现 —顺序存储 • 元素插入操作的时间复杂度分析 • 假设在第i个位置插入元素的概率为pi,则在长度为n 的线性表中插入一个元素所需移动元素次数的期望值为: • 假定在线性表中任何一个位置上进行插入的概率都是相等的,则移动元素的期望值为: • 在线性表中插入一个元素平均需要移动一半元素,时间复杂度是线性阶,即O(n)。
线性表的实现 —顺序存储 • 在顺序实现中:☆ insert 和 remove 的时间复杂度是O(n)。☆ List、clear、empty、full、size、replace和retrieve的时间复杂度是O(c)。 • 优点: ☆ 随机存取( retrieve ) • 缺点 ☆ 插入( insert )、删除( remove )需移动大量元素 ☆ 需要预先估计所需最大空间 ☆ 表的容量不能动态扩充 • 思考:如何实现容量可动态增长的顺序存储的线性表?
线性表的实现 —单链表 • 用一组地址任意的存储单元存放线性表中的数据元素。用附加的指针指示元素的前趋和后继关系。 • 为了表示数据元素ai 与其直接后继元素ai+1之间的逻辑关系,对数据元素ai 而言,除存储其本身的信息外,还需要存储指示其后继的指针(地址)信息。这两部分合起来通常称为结点。结点 = 元素 + 指针 • n个结点通过指针链接形成一个链表,即为线性表的链式存储结构。由于每个结点中包含一个指针域,所以称为单链表。
空指针 head a0 a1 … ... an^ 线性表的实现 —单链表 • 整个链表的存取必须从头指针开始进行,头指针指示链表中的第一个结点的存储位置。 • 最后一个元素由于没有后继结点,最后一个结点的指针应为空(NULL)。 • 在单链表中,数据元素之间的逻辑关系是由结点中的指针指示的,逻辑上相邻的两个元素,其存储的物理位置不必紧邻。 • 在线性表的顺序存储结构中,逻辑上相邻的元素物理位置紧邻,因而任何一个元素的存储位置都可以从线性表的起始位置计算得到,因而可实现随机存取。
线性表的实现 —单链表 • 在单链表中,任何两个元素的存储位置之间没有固定联系。每个元素的存储位置只能通过读取其前趋结点的指针域得到。 • 在单链表中,读取第i个数据元素必须从头指针出发寻找,单链表是非随机存取的存储结构。
线性表的实现 —单链表 • 在C++中定义单链表结点 • 对于线性表的客户程序而言,结点结构应该是不可见的,应把结点结构定义成链表类的私有嵌套结构。 template <typename list_entry>struct node { list_entry entry; //数据域node *next; //指针域node():next(0) {} node(const list_entry &le, node* link= NULL):entry(le), next(link) {}};
线性表的实现 —单链表 template <typename list_entry >class List {public: enum error_code { success, range_error, overflow, underflow};protected: struct node { list_entry entry; //数据域node *next; //指针域node():next(0) {} node(const list_entry &le, node* link= NULL):entry(le), next(link) {} }; int count; //线性表中元素个数 node *head; //链表头指针node *set_position(int i) const;public: //操作List(); ... void traverse( void (*visit)(list_entry&));};
线性表的实现 —单链表 • 为了实现单链表的其它操作,定义一个支持函数set_position,该函数的参数为元素的编号i,函数的功能是返回指向第i个元素的结点指针。 • set_position 是保护成员函数,只能用来实现类的其它成员函数,对单链表的客户是不可见的,因为它返回指向某个结点的指针,如果定义成公有成员,客户可以凭借该指针,直接修改结点内容,这是不安全的。 • 单链表是非随机存储结构,定位第i个结点,须从头指针开始遍历,直到找到指定的结点。 template <typename list_entry>List<list_entry>::node* List<list_entry>::set_position(int i) const { List<list_entry>::node *q = head; for (int j=0; j<i; j++) q = q->next; return q;}
线性表的实现 —单链表 template <typename list_entry >List<list_entry>::List():count(0),head(NULL) {} template <typename list_entry >int List<list_entry>::size() const { return count;} template <typename list_entry >bool List<list_entry >::empty() const { if ( count == 0 ) return true; return false;}
② ① 线性表的实现 —单链表 • 在单链表中插入无需大量移动元素。
线性表的实现 —单链表 template <typename list_entry >List<list_entry>::error_code List<list_entry >::insert(int i, const list_entry& x) { if ( i < 0 || i > count ) return range_error; node *newnode, *previous, *following; if ( i > 0 ) { previous = set_position( i-1); following = previous->next; } else following = head; newnode = new node(x,following); if ( newnode == 0 ) return overflow; if ( i == 0 ) head = newnode; else previous->next = newnode; count++; return success;}
线性表的实现 —单链表 • 在单链表中删除元素无需移动大量元素
线性表的实现 —单链表 template <typename list_entry>List<list_entry>::error_code List<list_entry>::remove(int i, list_entry& x) { if ( i < 0 || i > count ) return range_error; node* previous, *current; if ( i != 0 ) { previous = set_position( i-1 ); current = previous->next; previous->next = current->next; } else { current = head; head = head->next; } x = current->entry; delete current; count--; return success;}
线性表的实现 —单链表 • 清空单链表 template <typename list_entry>void List<list_entry>::clear() { node* nodeptr = head; while( head != NULL ) { head = nodeptr->next; delete nodeptr; nodeptr = head; } count = 0;}
线性表的实现 —单链表 • 在单链表中:☆ insert、remove、clear、replace和retrieve的时间复杂度是O(n)。☆ List、empty、full和size的时间复杂度是O(c)。 • 优点: ☆插入( insert)、删除(remove)无需移动大量元素。 ☆ 线性表容量仅受制于系统可供分配的存储空间大小。 • 缺点 ☆ 不支持元素随机存取 ☆ 需要存储指针的额外空间
线性表的实现 —改进的单链表 • 设想单链表客户程序需要多次读取(retrieve)链表中同一个元素,或总是按顺序读取单链表中的元素。 • 每次读取都需要从头指针开始遍历,效率不高。 • 可以对前述单链表实现进行改进,主要思想是记住最后一次存取过的元素的位置,如下一次存取的元素位于该元素之后,则从该元素开始遍历。若位于该元素之前,仍从头指针开始遍历。 • 注意该改进并非是在任何时侯都能提高存取效率。 • 在链表类中增加: • 指针current,保存上次访问过的结点地址 • 整型成员current_position,保存上次访问过的结点编号
线性表的实现 —改进的单链表 template <typename list_entry >class List {public: enum error_code { success, range_error, overflow, underflow};protected: struct node { ... }; int count; //线性表中元素个数 node *head; //链表头指针mutable int current_position; //上次访问过的元素的编号mutable node* current; //指向上次访问过的元素的指针void set_position(int i) const;public: //操作List(); ... void traverse( void (*visit)(list_entry&));};
线性表的实现 —改进的单链表 template <typename list_entry>void List<list_entry>::set_position(int i) const { if ( i < current_position ) { current_position=0; current = head; } for ( ; current_position<i; current_position++ ) current = current->next;} • set_position无需再返回指针,只要设置current指针即可。 • 新增的两个成员均是保护成员,对单链表的客户程序透明。 • 如果重复存取一个元素,在set_position中指针不用移动。 • 如果访问的元素位于记住的元素之前,未作任何改进。
线性表的实现 —双向链表 • 单向链表中,指针只能沿着一个方向移动,若找某结点的直接后继结点只须移动一次指针即可(O(1)),但要找某结点的直接前趋结点,则必须从头开始(O(n)) 。 • 为克服单向链表只允许单向移动指针的缺陷,引入双向链表。 • 在双向链表的结点中,有两个指针域,一个指向结点的直接前趋结点,另一个指向直接后继结点。
指向直接后继 head 指向直接前趋 空指针 线性表的实现 —双向链表 • 双向链表图示
线性表的实现 —双向链表 • 双向链表的结点设计 template <typename list_entry>struct node { list_entry entry; //数据域node *next; //指向后继的指针 node *back; //指向前趋的指针node():next(0), back(0) {} node(const list_entry &le, node* link_back=NULL, node* link_next=NULL):entry(le), back(link_back), next(link) {}};
线性表的实现 —双向链表 template <typename list_entry >class List { ...protected: struct node { list_entry entry; node* next; node* back; ... };protected:int count; //元素个数 mutable int current_position; //上次访问过的元素的编号mutable node *current; //指向上次访问过的元素的指针 void set_position(int i) const;public: //操作List(); ... void traverse( void (*visit)(list_entry&));};
线性表的实现 —双向链表 • 由于可以双向移动,双链表类中去掉了头指针,而只保留了current指针,从该指针出发可到达任何一个元素。 • 在双向链表中寻找第i个元素,首先应决定向前移动指针还是向后移动指针。 template <typename list_entry>void List<list_entry>::set_position(int i) const { if ( current_postion <= i ) for (; current_position != i; current_position++ ) current = current->next; else for (; current_position != i; current_position-- ) current = current->back;}
③ ② ① ④ 线性表的实现 —双向链表 • 双向链表的插入
线性表的实现 —双向链表 template <typename list_entry >List<list_entry>::error_code List<list_entry >::insert(int i, const list_entry& x) { node *newnode, *previous, *following; if ( i < 0 || i > count ) return range_error; if ( i==0 ) { if ( count==0 ) following=NULL; else { set_position(0); following=current; } previous=NULL;} else { set_position( i-1); previous=current; following = previous->next; } newnode = new node(x,previous,following); if ( newnode == 0 ) return overflow; if ( previous!=NULL ) previous->next = newnode; if ( following!=NULL ) following->back = newnode; count++; current = newnode; current_position=i; return success;}
线性表的实现 —双向链表 • 双向链表在单链表的基础上提高了效率,指针移动次数减少一半,时间复杂度仍为O(n)。 • 双向链表的结点中要保存两个指针,增加了额外空间。但如果数据域较大时,增加的空间量可忽略不计。
线性表总结 • 何时选用顺序实现? • 不需要作频繁的插入和删除元素操作(尾部的插入和删除除外)。 • 需要经常随机存取元素。 • 何时选用链式实现? • 结点数据域较大时。 • 经常需要作插入和删除操作 • 经常进行顺序存取。
上机作业 • 改进顺序存储的线性表,使之容量能够动态增长,在机器上用C++实现。 • 在机器上用C++分别实现改进的单链表和双链表。