《数据结构、算法与应用C++语言描述》线性表-链表描述

_11 《数据结构、算法与应用C++语言描述》线性表-链表描述

11表示第11篇博文,6表示在 数据结构算法与应用C++语言描述 书中所在章节。

本文包含了《数据结构、算法与应用C++语言描述》第六章主要练习题答案,给出了线性表链表描述完整测试代码。

6.1 线性表数据结构

6.1.1 定义

线性表(linear list)也称有序表(ordered list),它的每一个实例都是元素的一个有序集合。每一个实例的形式为( e 0 e_0 e0, e 1 e_1 e1,…, e n − 1 e_{n-1} en1),其中n是有穷自然数, e i e_i ei是线性表的元素,i是元素 e i e_i ei的索引,n是线性表的长度或大小。元素可以被看做原子,它们本身的结构与线性表的结构无关。当n=0时,线性表为空;当n>0时, e 0 e_0 e0是线性表的第0个元素或首元素, e n − 1 e_{n-1} en1是线性表的最后一个元素。可以认为 e 0 e_{0} e0先于 e 1 e_{1} e1 e 1 e_{1} e1先于 e 2 e_{2} e2,等等。除了这种先后关系之外,线性表不再有其他关系。

6.1.2 举例

  • 1)一个班级的学生按姓名的字母顺序排列的列表;
  • 2)按非递减次序排列的考试分数表;
  • 3)按字母顺序排列的会议列表;
  • 4)奥林匹克男子篮球比赛的金牌获得者按年代次序排列的列表。

6.1.3 线性表的抽象数据类型

抽象数据类型(abstract data type,ADT),既能说明数据结构的实例,也说明对它的操作。抽象数据类型的说明独立于任何程序语言的描述。所有对抽象数据类型的语言描述必须满足抽象数据类型的说明,抽象数据类型的说明保证了程序语言描述的有效性。另外,所有满足抽象数据类型说明的语言描述,都可以在应用中替换使用。

抽象数据类型linearList
{
实例
	有限个元素的有序集合
操作
    empty():若表空,则返回true,否则返回false 
    size():返回线性表的大小(表的元素个数)
    get(index):返回线性表中索引为index的元素
    indexOf(x):返回线性表中第一次出现的x的索引。若x不存在,则返回-1
    erase(index):删除索引为index的元素,索引大于index的元素其索引减1
    insert(index,x):把x插人线性表中索引为index的位置上,索引大于等于index的元素其索引加1
    output():从左到右输出表元素
}

6.1.4 抽象类linearList

抽象类

一个抽象类包含着没有实现代码的成员函数。这样的成员函数称为纯虚函数(pure virtual function)。纯虚函数用数字0作为初始值来说明,形式如下:

virtual int myPurevirtualFunction(int x)=0;

抽象数据类型不依赖程序语言,C++抽象类依赖程序语言。可以建立抽象类的对象指针。

抽象类的派生类,只有实现了基类的所有纯虚函数才是具体类,否则依然是抽象类而不能实例化。
具体类

具体类是没有纯虚函数的类。只有具体类才可以实例化。也就是说,我们只能对具体类建立实例或对象。

linearList抽象类定义

将抽象类的析构函数定义为虚函数,目的是,当一个线性表的实例离开作用域时需要调用的缺省析构函数时引用对象中数据类型的析构函数。

/*
Project name :			allAlgorithmsTest
Last modified Date:		2022年8月13日17点38分
Last Version:			V1.0
Descriptions:			线性表虚类
*/
#pragma once
#ifndef __LINEARLIST_H_
#define __LINEARLIST_H_
#include
using std::ostream;

template <class T>
class linearList
{
public:
	virtual ~linearList() {};
	virtual bool empty() const = 0;//返回true 当且仅当线性表为空
	virtual int size() const = 0;//返回线性表的元素个数
	virtual T& get(int theIndex) const = 0;//返回索引为theIndex的元素
	virtual int indexOf(const T& theElement) const = 0;//返回元素theElement第一次出现时的索引
	virtual void erase(int theIndex) = 0;//删除索引为theIndex的元素
	virtual void insert(int theIndex, const T& theElement) = 0;//把元素theElement插入线性表中索引为theIndex的位置上
	virtual void output(ostream& out) const = 0;//把线性表插入输入流out
};
#endif

6.2 链表描述

6.2.1 单链表

设L=( e 0 e_0 e0 e 1 e_1 e1,…, e n − 1 e_{n-1} en1)是一个线性表。在对这个线性表的一个可能的链式描述中,每个元素都在一个单独的节点中描述,每一个节点都有一个链域,它的值是线性表的下一个元素的位置,即地址。这样一来,元素 e i e_{i} ei的节点链接着元素 e i + 1 e_{i+1} ei+1的节点,0<=i e n − 1 e_{n-1} en1的节点没有其他节点可链接,因此链域的值为NULL。

每一个节点只有一条链,这种结构称为单链表

《数据结构、算法与应用C++语言描述》线性表-链表描述_第1张图片

原则上,数组适合查询比较频繁的数据存储,链表适合插入和删除比较频繁的数据存储,但是由于高速缓存容量的问题,导致单链表的插入和删除操作的运行时间高于数组。

也就是说,对于单表而言,性能比不上数组;但是对于某些特殊应用而言,比如把一个链表的尾节点和另一个链表的首节点链接起来,合并为一个链表,对于链表合并的时间复杂度为O(1),对于数组的时间复杂度比O(1)大。

《数据结构、算法与应用C++语言描述》线性表-链表描述_第2张图片

6.2.2 循环链表

1)把线性表描述成一个单向循环链表(singly linked circular list)(简称循环链表);只要将单向链表的尾节点与头节点链接起来,单向链表就成为循环链表。

2)在链表的前面增加一个节点,称为头节点(header node)。使用头节点的链表非常普遍,这样可以使程序更简洁、运行速度更快。

《数据结构、算法与应用C++语言描述》线性表-链表描述_第3张图片

6.2.3 双向链表

双向链表(doubly linked list)的每个节点都有两个指针:next 和previous。next指针指向右边节点(如果存在),previous指针指向左边节点(如果存在)。当双向链表只有一个元素节点p时,firstNode=lastNode=p。当双向链表为空时,firstNode=lastNode=NULL。

《数据结构、算法与应用C++语言描述》线性表-链表描述_第4张图片

6.3 链表应用

6.3.1 箱子排序

用链表保存班级学生清单。节点的数据域有:学生姓名、社会保险号码、每次作业和考试的分数、所有作业和考试的加权总分。假设分数是0~ 100的整数,要求按总分排序。

箱子排序(bin sort)就是首先把分数相同的节点放在同一个箱子里,然后把箱子链接起来就得到有序的链表。

通用箱子排序步骤:

1)逐个删除输入链表的节点,把删除的节点分配到相应的箱子里;

2)把每一个箱子中的链表收集并链接起来,使其成为一个有序链表。

链表箱子排序步骤:

1)连续删除链表的首元素,并将其插入相应的某个箱子的链表首位;

2)从最后一个箱子开始,逐个删除每个箱子的元素,并将其插人一个初始为空的链表的首位。

《数据结构、算法与应用C++语言描述》线性表-链表描述_第5张图片

稳定排序(stable sort):排序方法能够保持同值元素之间的相对次序,箱子排序就是稳定排序。

6.3.2 基数排序

定义

基数排序可以仅在Θ(n)时间内,就可以对0~ n c − 1 n^c-1 nc1之间的n个整数进行排序,其中c>=0是一个整数常量。如果用原来的箱子排序方法对range= n c n^{c} nc来排序,则复杂度为 Θ ( n + r a n g e ) = O ( n c ) Θ(n+range)=O(n^{c}) Θ(n+range)=O(nc)。扩展后的方法与binSort不同,它不直接对数进行排序,而是把数(number)按照某种基数(radix)分解为数字(digit),然后对数字排序。基数可以根据需要选择。

举例

例6-1 假定对0~999之间的10个整数进行排序。如果使用range=1000的箱子排序方法,那么箱子链表的初始化需要1000个执行步,节点分配需要10个执行步,从箱子中收集节点需要1000个执行步,总的执行步数为2010。而基数排序方法是:
1)利用箱子排序方法,根据最低位数字(即个位数字),对10个数进行排序。因为每个数字都在0~9之间,所以range=10。

2)利用箱子排序方法,对1)的结果按次低位数字(即十位数字)进行排序。同样有range=10。因为箱子排序是稳定排序,所以次低位数字相同的节点,按最低位数字排序所得到的次序保持不变。因此,现在的链表是按照最后两位数字进行排序的。

3)利用箱子排序方法,对2)的结果按第三位数字(即百位数字)进行排序。小于100的数,第三位数字为0。因为按第三位数字排序是稳定排序,所以第三位数字相同的节点,按最后两位数字排序所得到的次序保持不变。因此,现在的链表是按照后三位数字进行排序的。

《数据结构、算法与应用C++语言描述》线性表-链表描述_第6张图片

数字分解式

对于一般的基数r,相应的分解式为:

在这里插入图片描述

当使用基数r=n对0~ n c − 1 n^{c}-1 nc1范围内的n个整数进行分解时,每个数可以分解出c个数字。因此,对n个数,可以用c次range=n个箱子排序。因为c是一个常量,所以整个排序时间为Θ(cn)=Θ(n)。

6.4 课后习题–单链表实现

  • 2.编写方法chain::setSize(int theSize),它使线性表的大小等于theSize。若初始线性表的大小小于theSize,则不增加元素。若初始线性表的大小大于theSize,则删除多余的元素。计算方法的复杂度。测试你的代码。
  • 3.编写方法chain::set(theIndex,theElement),它用元素theElement替换索引为thelndex的元素。若索引theIndex超出范围,则抛出异常。计算方法的复杂度。测试你的代码。
  • 4.编写方法chain::removeRange(fromIndex,tolndex),它删除指定索引范围内的所有元素。
    计算方法的复杂度。测试你的代码。
  • 5.编写方法chain::lastIndexOf(theElement),返回值是指定元素最后出现时的索引。若这样的元素不存在,则返回-1。计算方法的复杂度。测试你的代码。
  • 6.重载操作符[],使得表达式x[i]返回对链表x的第i个元素的引用。若链表没有第i个元素,则抛出异常illegalIndex。语句x[i]=y和y=x[i]按以往预期的方式执行。测试你的代码。
  • 7.重载操作符==,使得表达式x==y返回true,当且仅当两个链表x和y相等,即对所有的i,两个链表的第i个元素相等。测试你的代码。
  • 8.重载操作符!=,使得表达式x!=y返回true,当且仅当两个链表x和y不等(见练习7)。测试你的代码。
  • 9.重载操作符<,使得表达式x
  • 10.编写方法chain::swap(theChain),它交换链表元素*this和theChain。计算方法的复杂度。测试你的代码。
  • 11.编写一个方法,它把数组线性表转换为链表。这个方法既不是类arrayList的成员函数,也不是类chain的成员函数。使用类arrayList的方法get和类chain的方法insert。计算方法的复杂度。测试你的代码。
  • 12.编写一个方法,它把链表转换为数组线性表。这个方法既不是类arrayList的成员函数,也不是类chain的成员函数。
    1)使用类chain的get方法和listSize方法,类arrayList的insert方法。计算方法的复杂度。测试你的代码。
    2)使用链表选代器。计算方法的复杂度。设计数据测试你的代码。
  • 13.在类chain中增加转换方法。一个方法是fromList(theList),它把数组线性表theList转换为链表。另一个方法是toList(theList),它把链表*this转换为数组线性表theList。计算方法的复杂度。测试你的代码。
  • 14.1)编写方法chain::leftShift(i),它将表中的元素向左移动i个位置。如果l=[0,1,2,3,4],那么l.leftShift(2)的结果是1=[2,3,4]。
    2)计算方法的复杂度。
    3)测试你的代码。
  • 15.1)编写方法chain::reverse,它颠倒*this中的元素的顺序,而且原地完成,不用分配任何新的节点空间。
    2)计算方法的复杂度。
    3)使用自己的测试数据检验方法的正确性。
  • 18.编写方法chain::meld。它生成一个新的扩展的链表c,它从a的首元素开始,交替地包含a和b的元素。如果一个链表的元素取完了,就把另一个链表的剩余元素附加到新的扩展链表c中。方法的复杂度应与链表a和b的长度具有线性关系。合并后的链表使用的应该是链表a和b的节点空间。合并之后,输入链表a和b是空表。
    1)编写方法meld,其复杂度应该与输入链表的长度具有线性关系。
    2)证明方法具有线性复杂度。
    3)测试代码的正确性。使用自己的测试数据。
  • 20.令a和b的类型为chain。假设a和b的元素类型都定义了操作符<、>、=、==和!=。而且假设a和b是有序链表(从左至右非逆减)。
    1)编写一个非成员方法merge,它生成一个新的有序链表c,包含a和b的所有元素。归并之后,两个输入链表a和b为空。
    2)计算方法的复杂度。
    3)使用自己的测试数据检验方法的正确性。
  • 22.令c的类型为扩展链表chain。
    1)编写一个成员方法split(a,b),它生成两个链表a和b。a包含*this中索引为奇数的元素,b包含*this中其余的元素。调用此方法后*this变为空链表
    2)计算方法的复杂度。
    3)使用自己的测试数据检验方法的正确性。
  • 23.在一个循环移动的操作中,线性表的元素根据给定的值,按顺时针方向移动。例如L=[0,1,2,3,4],循环移动2的结果是L=[2,3,4,0,1]。
    1)编写方法chain::circularShift(i),它将线性表的元素循环移动i个位置。
    2)测试你的代码。

6.5 单链表完整测试代码

见Github仓库:Github::Data-Structures-Algorithms-and-Applications/_2chain/

你可能感兴趣的:(数据结构,算法与应用,C++语言描述学习笔记,链表,数据结构,算法)