数据结构-线性表、栈和队列

线性表

由结点集N以及定义在结点集N上的线性关系R
(1)有一个唯一的开始节点,无前驱,有唯一后继
(2)对于有限集N 存在一个唯一的终止结点,无后继,有唯一前驱
(3)其他为内部结点,有唯一前驱和唯一后继
(4)线性表的节点个数称为长度,长度为0线性表称为空表
(5) 线性关系R简称前驱关系,应具有反对称和传递性

ADT定义

取值空间+运算集

template <class ELEM>
class list
{
	list()=default;
	~list();
	void clear();
	void append(ELEM value);//尾部增加新元素
	int length;
	void insert(int i,ELEM value);
	void remove(int i);
	ELEM fetch(int i);//返回值
}

存储结构

  1. 定长的一维数组结构 (向量型存储结构)
  2. 变长的线性存储结构 链接型存储结构。用前驱和后继关系将各个元素用指针连接起来

运算分类

(1) 创建实例
(2) 消除实例并释放空间
(3) 获取信息比如读取元素内容,由内容寻找位置
(4)访问线性表并改变内容或者结构
(5) 辅助操作 比如统计长度

顺序表

元素类型相同;元素顺序存储

类定义

每个元素占用L个存储单元,顺序表开始结点 k 0 k_0 k0的存储位置记为 b = l o c ( k 0 ) b=loc(k_0) b=loc(k0) 称为顺序表的首地址或者基地址,故下标为i的元素地址为 l o c ( k i ) = b + i × L loc(k_i)=b+i\times L loc(ki)=b+i×L
顺序表是一种随机存取的结构,两个物理位置相邻的元素互为前驱和后继,故物理相邻反映了逻辑相邻

运算

  1. 插入元素
    • 插入操作改变顺序表内容,所以除了被更新的元素,其他元素相对顺序应当不变,所以需要进行元素的移动以维护线性关系
    • 此外还需要检查插入操作的合法性,是否元素个数大于最大容量
    • 此算法开销主要在移动元素上,尾部插入移动0个元素,头部插入移动n-1元素,所以平均移动次数为 n / 2 n/2 n/2
  2. 删除元素
    • 首先检查是否空表
    • 删除操作也改变顺序表内容,所以元素要移动,平均移动次数是 ( n − 1 ) / 2 (n-1)/2 (n1)/2

链表

用一组任意的存储单元存储线性表的数据元素,其中存储数据元素的域称为数据域,存储直接后继位置的域称为指针域。n个这样的结点连接成一个链表。

单链表

  1. 如果每个结点只包含指向后继的指针,则称为单链表,头指针指示链表中第一个结点,整个链表的存取必须从头指针开始,因此单链表是非随机存取的结构
  2. 指针是数据元素之间的逻辑关系的映像,任何两个元素的存储位置没有固定的联系,因此两个逻辑上相邻的结点存储位置不一定相邻
template <class ELEM>
class ListNode
{
	ELEM val;
	ListNode<ELEM> *next;
	ListNode(ELEM _val, const LinkNode<ELEM>* nextptr=nullptr):val(_val),next(nextptr){}
	ListNode(const LinkNode<ELEM>* nextptr):next(nextptr){}
}
  1. 插入数据元素,首先生成一个结点,再将前驱结点指针域赋给该结点指针域,再将该结点地址赋给前驱结点指针域
void Insert(LinkedList &L, int i, int e)
{
	ListNode* p = FindIndex(L,i);
	auto ptr = new ListNode(L,e);
	ptr->next=p->next;
	p->next=ptr;
	return;
}
  1. 删除一个结点,仅需修改直接前驱的指针即可
void Delete(LinkedList &L, int i)
{
	ListNode* p = FindIndex(L,i);
	ListNode* p_prev = FindIndex(L,i-1);
	p_prev->next=p->next;
	if(p!=nullptr)
		delete p;
	return;
}
  1. 插入和删除算法的时间复杂度都为O(n)因为在第n个结点前插入或者删除结点,必须首先寻找到该结点的前驱

双链表

单向链表想找某结点的前驱需要从头结点开始遍历链表,故采用双向链表简化操作。双向链表中有两个指针域,一个指示直接前驱,一个指示直接后继
插入

void ListInsert(DualLink &d,int i, int val)
{	
	auto p =FindIndex(d,i-1);
	if(!p)
		reurn;
	auto q = new ListNode(val);
	q->next=p->next;
	q->prev=p;
	p->next->prev=q;
	p->next=q;
	return;
}
void ListDelete(DualLink &d,int i)
{
	auto p =FindIndex(d,i);
	if(!p)
		return;
	p->next->prev=p->prev;
	p->prev->next=p->next;
	p->next=nullptr;
	p->next=nullptr;
	delete p;
	return;
}

循环链表

循环链表的最后一个结点的指针域指向头结点,操作与线性链表基本一致,但是判断是否尾结点的条件为tail->next==head 而非tail->next==nullptr;

线性表实现方法比较

  1. 如果经常插入删除 不要使用线性表,线性表最大长度是重要因素
  2. 当随机访问操作比插入删除频率高,不使用链表; 如果指针存储开销比例较大,要慎重选择
  3. 一般来说 链表是线性表的首选存储结构,但是它不能随机访问,求长度时也不如线性表简单,且逻辑关系不能表示物理位置关系。

限制访问的线性表,LIFO,有插入和弹出操作,表首被称为栈顶,栈的另一端为栈底,
分为顺序栈和链式栈(指针方向从栈顶向下链接)

顺序栈

void Stack::Push(float item)
{
	assert(!IsFull());//判断栈满
	top++;
	ElmList[top]=item;
}
float Stak::Pop()
{
	assert(!IsEmpty());
	return ElmList[top--];
}

链栈

  1. 实际应用中顺序栈应用广泛,因为比较容易根据栈顶位置快速定位并读取栈的内部元素,读取元素所需时间为O(1) 而链式栈需要遍历
  2. 一般栈不允许访问内部元素,只能栈顶操作

栈的应用

  1. 递归实现表达式求值
    符号集合:0~9 和运算符号
    中缀表达式需要括号改变优先级
  • 中缀表达式转后缀表达式,如果是操作数直接输出到后缀序列,如果是开括号就压栈,如果闭括号先判断栈空,非空弹出元素直到遇到括号,把弹出的元素输出到后缀序列,
  • 后缀求值,遇到操作数压入栈顶,遇到运算符就从栈中取出两个操作数计算,结果压栈
  1. 栈与递归
    hanoi塔核心函数
void hanoi(int n,char X,char Y,char Z)
{
	if(n<=1)
		move(X,Z);
	else
	{
		hanoi(n-1,X,Z,Y);//X移动到Y 以Z为中转
		move(X,Z);
		hanoi(n-1,Y,X,Z)//Y移动到Z 以X中转
	}
}

队列

先进先出表,

template <class T>
class Queue
{
public:
 	void clear();
 	bool enQueue(const T item);
 	bool deQueue(T& item);
 	bool geFront(T& item);
 	bool isEmpty();
 	bool isFull();
}

链式队列不需要判断队列是否满,常用的存储结构有顺序队列和链式队列

顺序队列

  1. 两个变量指向队头和队尾,但是如果按顺序表的实现方法,如果从数组后面入队,出队列时间代价O(n)因为需要把前面的元素向前移动一个位置,入队列时间代价O(1)
  2. 如果不进行元素的移动,会出现假上溢出,就是front和rear都指向队尾,尽管此时队列为空,但也无法插入元素,所以引入循环队列
  3. 循环队列,某一个元素的直接后继是 ( x + 1 ) % m S i z e (x+1)\%mSize (x+1)%mSize,但是rear=front时无法判断队列满还是队列空,所以往往空出一个位置,以 ( r e a r + 1 ) % m S i z e = = f r o n t (rear+1)\%mSize==front (rear+1)%mSize==front作为队列满的标志

链式队列

栈与队列的深入讨论

你可能感兴趣的:(数据结构)