文章目录
重回大二–数据结构–线性表
0. 教材
电子工业出版社 数据结构算法与分析第三版 Clifford A.Shaffer
1. 定义
元素的数据项组成的一种有限且有序的序列。是一个具有n个数据元素的有限序列。
除第一个和最后一个元素外,其余元素都有且仅有一个直接前驱和直接后驱
书上用抽象类表示ADT 如下,注意任何继承该类实现的线性表都必须实现这些,否则会报错
#ifndef LIST_H
#define LIST_H
#include <iostream>
using namespace std;
namespace bisheng{
template <typename E>
class List
{
private:
void operator =(const List&) {}
List(const List&) {}
public:
List(){}//default constructor
virtual ~List(){}
virtual void lclear()=0;
virtual void lInsert(const E& item)=0;
virtual void lappend(const E& item)=0;
virtual void lprev()=0;
virtual void lnext()=0;
virtual E lremove()=0;
virtual void lmoveToStart()=0;
virtual void lmoveToEnd()=0;
virtual void lmoveToPos(int pos)=0;
virtual int lgetLength()const=0;
virtual int lcurrentPos() const =0;
virtual const E& lgetValue() const =0;
virtual void lprint() = 0;
};
}
#endif // LIST_H
题目
线性表的逻辑结构是:线性结构 其所含元素的个数称为线性表的长度
2. 两种实现方式
1. 基于数组
特点:
- 逻辑上相邻的元素,在物理中一定相邻
- 先声明数组长度,因而可能存在空间浪费,造成存储空间的碎片
- insert/remove时间开销θ(n),移动次数为n/2
- 快速获取某个位置元素或者其前驱后继
- 线性表的顺序存储结构是一种随机存取的存储结构,因为可以通过计算公式随机地取某个位置的元素. 获取第i个数据元素ai的存储位置:LOC(ai)=LOC(a1)+(i-1)*sizeof(ai)
2.基于链表
链表是一种用于存储数据集合的数据结构。链表中相邻元素之间通过指针链接,最后一个元素的后继指针为NULL(循环链表除外)
2.0 特点
- 用一组任意的存储单元存储线性表的数据元素
- 利用指针实现了用不相邻的存储单元存储逻辑上相邻的元素(我感觉和1无差别)
- 空间按需分配,无内存空间的浪费。但每个数据元素ai除存储本身信息外还有结构开销
- 访问某个位置或其元素的时间开销为θ(n)
- insert remove时间开销为θ(1)
2.1 单链表
2.1.1 非循环
有头结点的/无头结点的情况
- 头结点:在单链表的第一个结点前附设一的一个结点,作用是为更方便的操作链表。不一定是链表必须要素;
- 头指针head:是指链表指向第一个结点的指针,若链表有头结点,则是指向头节点的指针。无论链表是否为空,头指针均不为空,头指针是链表的必要元素;
- 最后一个结点:指针指向Null
题目:
- 不带头结点的单链表head 为空判定条件是:head==NULL
- 带头结点的单链表head为空条件:head->next==NULL
- 单链表不是一种随机存取结构 ☑️
2.1.2 循环单链表
带头指针的单循环链表
为空时
非空时:
缺点:用作队列时,在队列尾部添加元素时间复杂度高,需要遍历整个线性表,因此实现队列常用带尾指针的单循环链表。
题目:
循环链表的主要优点是:在表中从任意位置出发都能扫描整个链表
在一个以 h 为头指针的单循环链中,p 指针指向链尾结
在这里插入代码片
点的条件是()
A)p->next == NULL
B) p->next == h
C)p->next->next == h
D) p->data == -1算法设计题:设计一个算法将以链表实现单向链表按照elem的值从小到大排序
myanswervoid Lsort() { node<E> *first, *curr, *nxt,*left,*right; first = head->next; curr = first->next; if(!curr) return ;//一个元素就别排了 head->next->next = NULL; while(curr) { // 从第二个元素开始往后遍历 // 第一个循环中 curr 为第二个元素, nxt为curr的后一个 // first永远是第一个 nxt=curr->next; first=head->next; if (curr->elem <= first->elem) { curr->next = first; head->next = curr; } else { // 否则 找到一个比 currr 大的位置 left,right, 把 r 插在 left 的后一个 right前一个 left=first; right=first->next; while(right&& ((right->elem) < (curr->elem))) { left=right; right=right->next; } left->next=curr; curr->next=right; } curr = nxt; } }
2.2 双向链表
双向链表(double linked list)是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。所以双向链表中的结点都有两个指针域,一个指向直接后继,一个指向直接前驱。
2.2.1 非循环双链表
为空时
非空时
2.2.2循环双链表
-
为空时的情况
-
非空时
-
插入
题目
写出带头结点的双向循环链表为空表的条件:
L->next == L->prev == L
3. 特殊的线性表----栈
栈 (LIFO) sql: #include
3.1基本知识
特点:后进先出 ,是一种特殊的受限线性表,只能在一端(栈顶)进行插入或删除。
ps:栈是一种数据结构 栈中的元素有逻辑线性关系
-
定义:限定仅在一端进行插入或删除的线性表
-
数据关系:R1={ <ai-1, ai >| ai-1, ai∈D, i=2, … , n } 约定an 端为栈顶,a1 端为栈底。
-
基本操作:
- 判断是否为空 bool empty
- 返回元素个数 int length()
- 弹出栈顶 E pop()
- 在栈顶压入 bool push(const E &item)
- 返回栈顶元素 const E& top()
-
存储方式:
-
数组 由于事先声明大小,可能存在空间浪费.所有操作都是常数时间。
-
链表 每个元素都需要一个链接域,产生结构性开销。采用单链表即可,就在头部进行插入和删除,以免遍历整个链表
bool push(const E& item) { node <E> *temp=new node<E>;//失败会自己抛出badmalloc temp->next=top; temp->elem=item; top=temp; size++; return true; } E pop() { if(size==0) return false;//空栈 E it=top->elem; node<E> *temp=top->next; delete top; top=temp; size--; return true; }
-
-
术语
-
空栈与满栈
判断方式:
- top=0 //顺序栈为空 元素个数为0
- top==size/ /顺序栈为满,元素个数为n
- head->next==NULL //空
-
下溢
对空栈执行出栈操作
-
溢出
对满栈执行入栈
-
很明显栈太受限了,没有队列那么多考点,但是他的应用还是很重要的!
3.2 栈的应用
3.2.1 进制转化
基本公式:
N
=
(
N
/
d
)
∗
d
+
N
m
o
d
d
N=(N / d) *d + N \bmod d
N=(N/d)∗d+Nmodd
所以这就和栈非常相像了。
算法思想
- 输入一个十进制整数N
- 定义一个栈,甚至可以是char型(0-7)
- 只要当前N不为0,重复以下操作:N%8入栈,N=N/8
- 若栈非空,执行出栈并将出栈的内容输出
算法 描述
void convert(int N)
{
stack<char> s;
while(N!=0)
{
s.push(N %8);
N=n/8
}
while(!s.isempty)
{
cout<< s.pop();
}
}
算法分析
时间复杂度: O(logn)
空间复杂度:O(logn) 栈的开销
3.2.2 判别表达式中括号是否配对的算法
梦回编译原理语法分析。蠢蠢的代码,自己写的。。。
算法思想
- 输入字符 换行结束
- 对于输入的字符,入栈1
- 如果输入的是) ,只要栈1不空,没碰到第一个(前,把栈1里的字符压入栈2,同时栈1开始弹出
- 如果栈1空了还没有找到(,说明括号不匹配,因此输出错误
- 如果找到了,将栈2的内容弹出,输出该括号匹配的字符
- 最后输入完成后,检查栈1.如果栈1里还有多余的(,说明括号不匹配,输出错误信息
算法描述
void match()
{
stack<char> s,s2;
char ch;;
while(ch=cin.get())
{
s.push(ch);
if(ch == ')')
{
while(!(s.empty()))
{
s2.push(s.top());
s.pop();
if(s2.top()=='(') break;//找到匹配的左括号了
}
if(s2.top()!='(') cout<<"false , lack of '('"<<endl;
else
{
while(!s2.empty())
{
cout<<s2.top();
s2.pop();
}
cout<<endl;
}
}
if(ch=='\n') break;
}
while(!s.empty())
{
if(s.top()=='(') {cout<<"false , lack of ')'"<<endl;break;}
s.pop();
}
}
常考题目
- 判断这种出栈顺序有可能不
- 将递归算法转换为对应的非递归算法需要 栈(函数的递归调用本身就在用栈)
3.3.3 乱入— 后缀表达式
我也不知道哪来的,据说是因为后缀表达式的计算是看到数字入栈,看到n元操作符就出栈n个栈内的东西进行运算后再入栈。所以比如这个表达式1 2 3 + * 6 -,1,2,3入栈,看到+出栈2 3 计算后结果为5 入栈 ,看到* 出栈5 1 ,进行运算结果为5入栈,看到6入栈,看到-,出栈6 5 ,进行5-6(注意顺序了!!)。最后-1入栈。
但是一个我们平时见到的表达式,如何表示为 后缀表达式?
例题:
a*(b+c)-d
-
第一步 按照计算规则加括号:
((a*(b+c))-d)
-
第二步 把运算符移到括号外 (所以刚写的代码还是有用啊!)
((a(bc)+)*)d)-
-
去括号
abc+*d-
网上也有方法是,先根据中序遍历把这个表达式的二叉树画出来(but 已知中序不能画出唯一的二叉树,如a*(b+c+d)),然后再后序遍历这个二叉树
4. 特殊的线性表—队列
4.1 基本知识
特点:先进先出 时间上越早产生的对象越早被处理。队列中的元素有逻辑线性关系。
-
定义
队列(queue )简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。
-
数据关系
数据关系: R1={ <a i-1,ai > | ai-1, ai∈D, i=2,…,n} 约定其中a1 端为队列头,an 端为队列尾
-
基本操作
virtual void clear() = 0; virtual bool enqueue(const Elem&) = 0; virtual bool dequeue(Elem&) = 0; virtual bool frontValue(Elem&) const = 0; virtual int length() const = 0;
-
术语
插入端:队尾
删除端:队首
入队操作:enqueue 出队操作:dequeue
4.2 队列的实现
因为队列的实现相比栈稍微复杂一点,所以专门列了一个模块。注意到我的代码是没有头结点的单链表。
4.2.1 基于链表实现
template <typename E>
bool Lqueue<E>::dequeue(E&it)
{
if(Size==0) return false;
it=head->elem;
node<E> *temp=head;
head=temp->next;
delete temp;
Size--;
if(Size==0) tail = NULL;//否则tail会变成野指针
return true;
}
template <typename E>
bool Lqueue<E>::enqueue(const E &it)
{
node<E> *temp=new node<E>(it,NULL);
if(Size==0)
{
tail=head=temp;
}
else
{
tail->next=temp;
tail=temp;
}
Size++;
return true;
}
4.2.2 基于顺序表实现—循环队列
因为头部弹出,尾部增加,如果不设置为循环队列,很有可能最后数组大部分地址是空的也加不进去
循环队列首先需要一个front和rear来存储队首元素和队尾元素的位置编号。注意点:如何判断队列空/满。
空:head==rear -1
rear | head | null | null | null | null | null | null |
---|---|---|---|---|---|---|---|
满情况有很多种:插入-1 0 1 2 3 4 5 6,弹出-1 0插入 7 8
7 | 8 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
rear | head |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|
rear | head |
很明显情况一和空的情况重合了,无法判断。解决办法,开n+1的数组,只放n个元素
那么判断空:rear==front-1
判断满: (rear+2)% maxsize==front
4.3 队列的应用
4.3.1 打印杨辉三角
梦回ccf夏季小学期。用数组他不香吗。。。
输入n。
首先杨辉三角形第i行第j个元素是i-1行的第j-1和第j个的元素的和。
然后一般在两头 插入 0,方便直接计算。
输入: 8
输出:
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1
算法思想:
-
aij=ai-1 j-1+ai-1 j
-
首先把第一行的1进入队列 然后打印队列,既三角形第一行
-
当前待处理行为2
-
然后开始处理当前待处理行
-
为了方便进行计算,用一个变量s记录ai-1 j-1 ,初始置0。
-
为了方便直接从队列中取值运算,在队列中入队0,相当于在上一行最后加一个0.
-
每次取出队列中队头的元素,然后将其和变量s相加,结果存入队列。然后更新变量s的值为刚刚取出的队头。执行步骤5直到输出完这一行该输出的数字,既第i行进行i次循环
-
打印队列里当前所有元素。
-
当前待处理行+1,重复执行2-6,直到处理完第n行
算法代码:
void printRec(int n )
{
Lqueue<int> a;
a.enqueue(1);//入队
a.print();//打印第一行
int temp=0,s=0;
for(int i=1; i<n; i++)
{
s=0;
a.enqueue(0);
for(int j=0; j<=i; j++)
{
a.dequeue(temp);//temp存储出队的元素值
a.enqueue(temp+s);//temp+s入队
s=temp;//更新s值
}
a.print();//打印第i+1行
}
}
算法分析
- 时间复杂度:O(n2)
- 空间复杂度:队列的开销 0(n)
4.3.2 倒置
栈的ADT函数有:
void makeEmpty(SqStack s); 置空栈
void push(SqStack s,ElemType e); 元素e入栈
ElemType pop(SqStack s); 出栈,返回栈顶元素
bool isEmpty(SqStack s); 判断栈空
队列的ADT函数有:
void enQueue(Queue q,ElemType e); 元素e入队
ElemType deQueue(Queue q); 出队,返回队头元素
bool isEmpty(Queue q); 判断队空
void convert(Queue a)
{
stack<ElemType> b;
ElemType temp;
while(!a.isEmpty())
{
temp=a.dequeue();
b.push(temp);
}
while(!b.isEmpty())
{
temp=b.pop();
a.enqueue(temp);
}
b.makeEmpty();
}