数据结构与算法(2)-线性表

Author:Liedra
https://www.cnblogs.com/LieDra/

Ch2 线性表


0x01 线性表的定义和特点


定义

线性表是一种可以在任意位置进行插入和删除数据元素操作、由n(n≥0)个相同类型数据元素a0,a1,…,an-1组成的线性结构。是最常用且最简单的一种数据结构,它是n个数据元素的有限序列。
操作集合:初始化线性表、插入数据元素、求当前数据元素个数、删除数据元素、取数据元素等。
实现方式:线性(数组)、链式(指针)。
注意,下两节所说的存取操作和插入删除操作指的是在某个位置的操作,不是对某个值的操作。


0x02 线性表的顺序表示和实现(顺序表)


定义

使用数组,用一段地址连续的存储单元依次存储线性表的数据元。(一般采用静态数组方法。

编号地址

存储器中的每个存储单元都有自己的编号,这个编号称为地址。

存储位置公式

每个数据元素,不管它是整型,实型还是字符型,都需要占用一定的存储单元空间(假设为x)
那么第i(从0计算)个元素的存储位置为
indexi = index0 + i*x 从这个公式可以得到此时存取操作时间性能为O(1)即常数性能,因为计算机只需要计算一次。
我们通常将存取操作具备常数性能的存储结构称为随机存储结构

顺序表类的定义

类的设计包括两部分:类定义和类实现。类定义给出类的成员变量和成员函数的定义,类实现给出成员函数的具体编码实现。
类的成员变量用来表示抽象数据类型的数据集合,类的成员函数用来表示抽象数据类型的操作集合

class SeqList       //SeqList是类名
{
protected:
	DataType *list;                           //数组元素的数据类型
	int maxSize;                              //最大元素个数
	int size;                                 //当前元素个数
public:                                       
	SeqList(int max=0);                       //构造函数
	~SeqList(void);                           //析构函数
	int Size(void) const;                      //取当前数据元素个数
	void Insert(const DataType& item,int i);  //插入
	DataType Delete(const int i);             //删除
	DataType GetData(int i) const;            //取数据元素
};

顺序表类的实现


SeqList::SeqList(int max)                   //构造函数
{
	maxSize = max;
	size = 0;
	list = new DataType[maxSize];
}

SeqList::~SeqList(void)                       //析构函数
{
	delete[]list;
}

int SeqList::Size(void) const        //取当前数据元素个数
{
	return size;
}

void SeqList::Insert(const DataType& item, int i)    //插入
//在指定位置i前插入一个数据元素item
{
	//从size-1至i逐个元素后移
	for (int j = size; j > i; j--) list[j] = list[j - 1];
	list[i] = item;                                   //在i位置插入item
	size++;                  //当前元素个数加1
}

DataType SeqList::Delete(const int i)      //删除
//删除指定位置i的数据元素,删除的元素由函数返回
{
	DataType x = list[i];
	//取到要删除的元素
//从i+1至size-1逐个元素前移
	for (int j = 1; j < size - 1; j++) list[j] = list[j + 1];
	size--;                                //当前元素个数减1
	return x;                              //返回删除的元素
}

DataType SeqList::GetData(int i) const    //取数据元素
//取位置i的数据元素,取到的数据元素由函数返回
{
	return list[i];                       //返回取到的元素
}

注意:
(1)类的成员变量通常设计成私有(private)访问权限,该类要被作为基类继承,所以设计成保护(protected)访问权限。 (2)构造函数要完成对象定义以及初始化赋值 (3)对于动态数组存储结构,析构函数要释放动态申请的内存空间 (4)插入成员函数应该考虑插入位置参数i的范围以及数组的存储空间是否已满

效率分析

时间效率:算法时间主要耗费在移动元素的操作上

  • 存取操作:O(1)
  • 插入和删除:O(n),因为插入或删除后,需要移动其余元素,有n中位置插入。

优缺点和适用场景

优点:算法简单,空间单元利用率高;
缺点:需要预先确定数据元素的最大个数,插入和删除时需要移动较多的数据元素。

线性表顺序存储结构比较适用于元素存取操作较多,增删操作较少的场景

顺序表类应用举例

编程实现如下任务:建立一个线性表,首先依次输入数据元素1,2,3,…,10,然后删除数据元素5,最后依次显示当前线性表中的数据元素。要求采用顺序表实现,假设该顺序表的数据元素个数在最坏情况下不会超过100个。

实现的方法,利用已设计好的抽象数据类型模块(存放在头文件名为SeqList.h中,通过 #include “SeqList.h” ),直接编写一个主函数实现。
程序如下:

#include <iostream>
#include <stdlib.h>

using namespace std;
typedef int DataType;           //定义具体问题元素的数据类型
#include "SeqList.h"            //包含顺序表类
int main()
{
	SeqList myList(100);        //定义顺序表类对象myList
	int n = 10;
	for (int i = 0; i < n; i++)     //在myList中顺序插入10个元素
		myList.Insert(i + 1, i);
	myList.Delete(4);           //删除myList中数据元素5
	for (int i = 0; i < myList.Size(); i++)
		cout << myList.GetData(i) << " ";
	system("pause");
	return 0;
}

注意,使用下列语句使得抽象数据类型实际上转为int类型
typedef int DataType;


0x03 线性表的链式表示和实现(单链表)


定义

一个或多个结点组合而成的数据结构称为链表。单链表中构成链表的结点只有一个指向直接后继结点的指针域。其结构特点:逻辑上相邻的数据元素在物理上不一定相邻。

结点一般由两部分构成:

  • 数据域:存储真正的数据元素
  • 指针域:存储下一个结点(直接后继)的地址(指针)
    头指针:是指向链表中第一个结点(或为头结点、或为首元结点)的指针。之后的每一个结点,其实就是上一个的后继指针指向的位置。链式存储时只要不是循环链表,就一定存在头指针。
    头结点:为了能更加方便地对链表进行操作,会在单链表的首元结点前附设一个结点,称为头结点,其数据域一般无意义(有时也会存链表长度等,头结点不计入表长度)。
    元结点是指链表中存储线性表第一个数据元素a0的结点。
    头指针与头结点:当不存在头结点时,头指针指向首个有真实数据的结点,当存在头结点时,头指针指向头结点,头结点的指针域指向首个有真实数据的结点。

存在头结点时:
图片2.png

不存在头结点时:
图片3.png

结点类的定义和实现

//List.h
#include<iostream>
template<class T>class List;        //前向引用声明
template<class T>
//Node类的定义和实现
class Node
{
public:
	friend class List<T>;                            //List类为友元类
	Node(Node<T>* Next = NULL) { next = Next; }       //构造函数一,构造头结点,没有data数据
	//构造函数二,构造其他结点,有data数据
	Node(T& Date, Node<T>* Next = 0)
	{
		date = Date;
		next = Next;
	}
	~Node() {}
private:
	T date;
	Node<T>* next;           //next指向下一个结点
};

单链表类的定义

这里实现的单链表其实是有序单链表,插入数据是先找到应该插入的位置,再进行插入,非有序的情况下的单链表的实现更加简单。

template<class T>
class List
{
private:
	Node<T>* head;                                    //头指针
	int size;                                         //数据元素的个数
public:
	List();                                           //构造函数
	~List();                                          //析构函数
	Node<T>* Index(int i);                            //定位
	void Insert(T& Data);                             //插入
	T Delete(int i);								  //删除数据并返回结果
	T GetData(int i) { return Index(i)->data; }       //获得数据
	//这里还可以根据需要添加Get size函数
};

单链表类的实现

实现是如果是带头结点的,那么构造函数要用new运算符动态申请一个头结点并由头指针指示,初始时当前数据元素个数为0。

析构函数要完成单链表中所有结点内存空间的释放。

插入的实现:首先找到第i个结点并由Index(i)指示,动态申请一个新的结点,其next指向第i个结点,然后修改i-1结点的指针域指向新结点。

//List类的实现
template<class T>
List<T>::List()
{
	head = new Node<T>();            //头结点指向一个Node类
	size = 0;                        //初始时数据元素个数为0
}
template<class T>
List<T>::~List()
{
	Node<T>* p;
	while (head != 0)                //while删除当前链表的第一个结点,直到head=0,没有结点
	{
		p = head;
		head = p->next;
		delete p;
	}
	size = 0;                        //数据元素个数为0
}
template<class T>
Node<T>* List<T>::Index(int i)
{              //定位,目的是找指向第i个数据元素的指针并返回
	if (i<-1 || i>(size - 1))                //i的范围为-1到size-1
	{
		cout << "错误的i值" << endl;
		exit(0);                             //错误的话结束
	}
	if (i == -1)return head;                 //i=-1时,指向头结点
	Node<T>* p = head->next;                 //p首先指向第0个数据元素结点
	int j = 0;
	while (p != 0 && j < i)                  //循环,找到指向第i个结点的指针
	{
		p = p->next;
		j++;
	}
	return p;                                //返回指向第i个结点的指针
}

template<class T>
void List<T>::Insert(T& Data)                //插入数据,这里的插入是按照大小顺序插入的
{
	int i = 0;                                                       //从0开始计数
	this->size++;                                                    //插入数据时该链表数据元素个数加一,这样定位时新加的元素如果在最后,则Index返回指针是空指针
	while (Index(i) != 0 && Index(i)->data <= Data) { i++; }         //找比data大的所在的结点的指针
	Node<T>* p = new Node<T>(Data, Index(i));                        //构造新的结点
	Index(i - 1)->next = p;                                          //新结点前面的结点的next指向新结点
}

template<class T>
T List<T>::Delete(int i)
{
	Node<T>* s, * p = Index(i - 1);       //p为指向第i-1个结点指针
	s = p->next;                            //s指向第i个结点
	p->next = p->next->next;                //第i个结点脱链
	T x = s->data;
	delete s;                             //释放第i个结点空间
	size--;                               //结点个数减1
	return x;                             //返回第i个结点的data域值
}

效率分析

时间复杂度:插入和删除操作时不需要移动数据元素,只需要比较数据元素。

  • 存取操作:假设需要获取第 i 个元素,则必须从第一个结点开始依次进行遍历,直到达到第 i 个结点。因此,对于单链表结构而言,其数据元素读取的时间复杂度为O(n)
  • 插入和删除操作:对其任意一个位置进行增删操作,因为需要先遍历找到目标元素,其时间复杂度为O(n)。

顺序表和单链表比较

只对一个元素进行增删操作时,两种结构并不存在优劣之分,而如果针对多个数据进行增删,那么单链表较为合适,因为后续的增删只是简单的赋值移动指针。
单链表的优点:不需要预先确定数据元素的最大个数,插入和删除操作不需要移动数据元素;
单链表的缺点:每个结点中要有一个指针域,因此空间单元利用率不高。而且单链表操作的算法也较复杂。
总结:对于存取频繁的场景,使用线性表较好,对于插入和删除频繁的场景,使用单链表一般较好。

顺序表类应用举例

编程实现如下任务:建立一个线性表,首先依次输入数据元素1,2,3,…,10,然后删除数据元素5,最后依次显示当前线性表中的数据元素。要求采用单链表实现。
结点类和单链表类的定义和实现代码存放在文件List.h中,通过直接编写一个主函数来实现。
程序如下:

#include <iostream>
using namespace std;
#include"List.h"
int main()
{
	List <int> myList;
	int s[] = { 1,2,3,4,5,6,7,8,9,10 }, n = 10;
	int temp;
	for (int i = 0; i < n; i++)
		myList.Insert(i);
	myList.Delete(4);
	for (int i = 0; i < 9; i++)
	{
		temp = myList.GetData(i);
		cout << temp << " ";
	}
	cout << endl;
	system("pause");
	return 0;
}

结果

0 1 2 3 5 6 7 8 9
请按任意键继续. . .

0x04 循环单链表和双向链表


循环单链表

实现:循环单链表是单链表的另一种形式,将单链表中的终端结点的指针域由空指针改为指向头结点,使整个单链表形成一个环,这种头尾相连的单循环链表,称为循环链表。循环链表不一定需要头结点。
单链表区别:主要差异在于循环的判断条件上,单链表判断条件为判断尾结点是否指向空,循环链表判断条件为判断结点是否指向头结点(头指针)
优点:从链尾到链头比较方便。

双向链表

双向链表是每个结点除后继指针域外还有一个前驱指针域,它有带头结点和不带头结点,循环和非循环结构,双向链表是解决查找前驱结点问题的有效途径。在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
p->next->prior=p;p->prior->next=p


0x05 静态链表


在数组中增加一个(或两个)指针域用来存放下一个(或上一个)数据元素在数组中的下标,从而构成用数组构造的单链表。因为数组内存空间的申请方式是静态的,所以称为静态链表,增加的指针称做仿真指针。


0x06 其他

在上述顺序类的基础上,还可以通过设计完成相应功能的函数,来达到对顺序表中某些数据元素x的删除,将单链表原地排序等功能。具体设计要根据实际情况进行。

posted @ 2020-06-21 20:03  LieDra  阅读(509)  评论(0编辑  收藏  举报