线性表:多个元素的序列
以C++模板类复现了顺序表、单链表、循环单链表、循环双链表
实现线性表这样的数据结构,我们需要考虑数据元素怎么存?在实现数据元素的增删查改操作时,必须保证线性结构不改变,一一对应。封装成为结构,我们可以更加关注整体业务逻辑,而不是对关注实现底层数据的增删查改!
元素之间是有顺序的:除了第一个元素无直接前驱元素,最后一个元素无直接后继元素外,其余所有元素有且仅有一个前驱后继元素。
线性表的两个属性:
线性表长度:线性表元素个数,随着对线性表插入删除的操作,线性表的长度在变化,为此在实现数据结构时,通常需要为线性表添加长度属性
线性表元素位序:每个元素都对应着一个位序,即元素在线性表中的位置,从1开始取值。位序这个属性丰富了线性表的操作:比如,根据位序获取数据元素,查找某个元素是否在线性表中,若存在返回元素的位序,在给定的位序出插入删除数据元素。
对于线性表,有两种常用的物理存储结构:顺序存储结构和链式存储结构
如果采用顺序存储结构,使用一片连续的内存空间来依次存储线性表中的数据元素,由于是连续内存空间,物理位置相邻关系就可以表示出线性关系(仅有一个前驱和后继数据元素)。如何编程实现线性表的顺序存储?高级语言的数组可帮助我们在内存中得到一个连续的内存空间,在编程语言中数组名称代表内存空间的起始地址,数组长度代表在划分的最大内存容量。
顺序存储的存取时间复杂度为O(1):线性表特意指出数据元素属于同种类型,因此使用数组存储数据元素时,只要有数据首个元素在内存中的起始位置就可以通过计算公式取出指定位序的元素或是向其中存入指定值。
//一个简单的顺序线性表(单链表)
const int S = 20;
template<class T>
class ArrayList{
T data[S];
int length;
public:
ArrayList(int n) {
length = n;
}
bool getValue(int i,T & result) {
if (0 < i <= length)
{
result = data[i - 1];
return true;
}
else
return false;
}
int find_ele(const T &ele) {
for (int i = 0; i < length; i++)
{
if (ele == data[i])
return i;
}
return -1;
}
bool insert(const T & ele,int i ) {
if (S == length||i<=0||i>length)
return false;
if (0 < i <= length)
{
int k;
for ( k = length - 1; k >=i - 1; k--)//小心处理头部和末尾元素
{
data[k + 1] = data[k];
}
data[i-1 ] = ele;
length++;
return true;
}
}
bool del_ele(const int i, T & ele) {
if (0 < i <= length)
{
ele = data[i - 1];
for (int k = i - 1; k < length - 1; k++)
{
data[k] = data[k + 1];
}
length--;
return true;
}
else
return false;
}
int get_len() {
return length;
}
};
#include "stdafx.h"
#include"ArrayList.h"
#include<iostream>
using namespace std;
int main()
{
ArrayList<int> a(5);
cout << " list length is:" << a.get_len() << endl;
//a.insert(0, 0);
a.insert(1, 1);
a.insert(2, 2);
cout << " list length is:" << a.get_len() << endl;
int x;
a.getValue(1, x);
cout << x<< endl;
cout<<a.find_ele(2) << endl;
a.del_ele(1, x);
cout << x << endl;
cin.get();
return 0;
}
线性表的另一种存储方式:链式存储
使用指针表示逻辑关系
头结点:为了使得头尾特殊元素的插入删除操作与其余元素操作一致而引入的概念
头指针:链表首个元素的地址
多个数据元素组成单链表如何表示呢?(定义单链表结构的思想值得学习)
定义出数据元素的结构,由于数据元素在内存中是离散的,他们之间的关系通过一个额外的数据项(指针)表出,与顺序结构不同,为了表出线性表,分配了连续的内存空间,链式结构在构造每个元素的时候就存储好哥各个元素的关系,因此我们只需要一个指向链式线性表的指针(指向链式结构的首个元素),就可以表出整个线性表,思想很好啊,在存储数据元素的时候通过额外数据项来表示关系,不再需要连续存储,只需要一个指针就可以表示整个线性表!
//指针 地址号 有了指针可以对其存储的数据进行修改 利用多个指向同一内存的指针来达到修改数据效果
template <typename T>
struct Node{
T data;
Node<T> * next;
};
template <typename T>
class LinkList {
Node<T> * head;//头指针
int length;
public:
LinkList() {
head = new Node<T>;//卡壳了
head->next = nullptr;
}
LinkList(int len)
{
head = new Node<T>;
head->next = nullptr;
Node<T> *q;
for (int i = 1; i <=len; i++)//头插法
{
q = new Node<T>;
q->data = i;
q->next = head->next;
head->next = q;
length++;
}
}
void initial(int len) {//尾插法
//Node<T> * head;//不懂脑子 利用惯性思维的结果就是排了好久的错 名称遮掩问题
head = new Node<T>;
//head->next = nullptr;
Node<T> * tail=head;//尾节点 写法值得学习的 使用tail指针来找到尾节点 通过这个尾节点串联整个链表 数据元素存在单个节点 节点间通过指针连接 因此创建链表的时候需要一个指针表示末尾元素 修改指针的数据域 更新末尾元素所在内存地址
Node<T> *q;
for (int i = 1; i <=len; i++)
{
q = new Node<T>;
q->data = i;
//q->next = nullptr;
tail->next = q;//连成一个链表 更新末尾数据元素的数据域
tail = q;//更新这个链表的末尾元素所在地址
length++;
}
tail->next = nullptr;
}
~LinkList() {
delete head;
}
bool get_value(int i ,T & ele) {
if (0 < i <= length)
{
Node<T> * temp;
temp = head->next;
int k = 1;
while (temp)
{
if (k==i)
{
ele = temp->data;
return true;
}
k++;
temp = temp->next;
}
}
else
return false;
}
int find_ele(const T &ele) {
Node <T> * temp;
temp = head->next;
int i = 1;
while (temp)
{
if (temp->data == ele)
{
return i;
}
temp = temp->next;
i++;
}
return -1;
}
bool insert(const T & ele, int i) {
Node<T> * temp;
Node<T> * p;
Node<T> * q;
if (0 < i <= length + 1) { //通过头结点,使得对末尾元素操作与其余元素一致
temp = new Node<T>;
p = head->next;
int k = 1;
for (; k < i - 1; k++)
{
p = p->next;
}
q = p->next;
temp->data = ele;
temp->next = q;
p->next = temp;
length++;
return true;
}
else
return false;
}
bool del_ele(T & ele ,int i) {
if (0 < i <= length) {
Node<T> * p;
Node<T> * q;
p = head->next;
int k = 1;
for (; k < i - 1; k++)
{
p = p->next;
}
q = p->next;
p->next = q->next;
ele = q->data;
delete q;
length--;
return true;
}
else
return false;
}
int get_len() {
return length;
}
};
#include "stdafx.h"
#include"LinkList.h"
#include<iostream>
using namespace std;
int main()
{
LinkList<int > a;
a.initial(5);
int x;
a.get_value(1, x);
cout << x << endl;
a.insert(32, 3);
cout << a.find_ele(32) << endl;
a.del_ele( x,2);
cout << x << endl;
cout<<a.get_len();
cin.get();
return 0;
}
循环链表:
尾指针代替头指针(下面将讲原因)
单链表最后一个元素的指针域为空,这说明从链表中任意位置出发无法访问到所有链表中的元素,想要访问所有的数据元素只能从头结点开始,否则不可能!为了达到能从任意位置出发就可以访问到链表中所有元素,提出了循环链表(我们需要一种结构(重新设计一种结构)能支持我们可以不再最后一个数据元素就终止),现实生活场景处处包含这样的需要。
循环链表是线性表的另一种存储结构,虽然只是在单链表的基础上增加一些操作使得末尾元素不再是空指针而是指向头结点。
在上面实现里,没有循环指针我们也是可以访问到所有数据元素,不过复杂度高,为此提出了循环链表,既然都有了循环概念,能否进一步增加这种结构的灵活性和降低时间复杂度呢?通过一个指向末尾节点的指针,尾指针,有了尾指针,访问末尾元素的时间复杂度立刻从O(n)变为O(1)且访问头元素的时间复杂度依旧是O(1),循环链表利用尾指针替代头指针是有意义的,但单链表设计尾指针也没啥太大意义,除了在访问链表的最后一个元素上有好的性能,也没别的用途了,也不能利用这个尾指针访问其余数据元素。
template <typename T>
struct Node {
T data;
Node<T> * next;
};
template<typename T>
class LoopList {
Node<T> * tail;//使用尾指针代替头指针
int length;
public:
LoopList() {
tail = new Node<T>;
tail->next = tail;
length = 0;
}
LoopList(int n) {//尾插法
tail = new Node<T>;
tail->next = tail;
for (int i = 1; i <= n; i++)
{
Node<T> *p = new Node<T>;
p->data = i;
p->next = tail->next;
tail->next = p;
tail = p;
length++;
}
//tail->next = tail;
}
bool get_value(int i, T & ele)
{
if (0 < i <= length)
{
Node <T> * p = tail->next->next;
for (int k = 1; k < i; k++)
{
p = p->next;
}
ele = p->data;
return true;
}
else
return false;
}
bool insert(int i,const T & ele) {
if (0 < i <= length) {
Node<T> *p = tail->next;
int j = 0;
for (; j < i - 1; j++) {
p = p->next;
}
Node<T> *q = new Node<T>;
q->data = ele;
q->next = p->next;
p->next = q;
length++;
return true;
}
else
return false;
}
bool del_ele(T & ele, int i) {
if (0 < i <= length) {
Node<T> *p = tail->next;
for (int k = 1; k < i; k++) {
p = p->next;
}
Node<T> *q = p->next;
ele = q->data;
p->next = q->next;
delete q;
length--;
return true;
}
else
return false;
}
void merge(LoopList<T> *another) //编程时候总是忘记自己的操作目标及对象 我的目标是在一个链表基础上合并两个链表 但却忘记了改变尾指针
{
Node <T> *temp = tail->next;
Node<T> *q=another->tail->next;
tail->next = q->next;
delete q;
another->tail->next = temp;
tail = another->tail;
length += another->get_len();
}
int get_len() {
return length;
}
int find_ele(const T & ele) {
Node<T> *p = tail->next->next;
int k = 1;
while (p) {
if (p->data == ele)
return k;
k++;
p = p->next;
}
return -1;
}
};
#include "stdafx.h"
#include"LoopList.h"
#include<iostream>
using namespace std;
int main()
{
LoopList <int > a(5);
LoopList<int > b(2);
a.merge(&b);
cout << a.get_len() << endl;
int x;
a.get_value(7, x);
cout << x << endl;
a.insert(2, 66);
cout << a.find_ele(66) << endl;
a.del_ele(x, 2);
cout << x << endl;
cin.get();
return 0;
}
再次对我们的循环链表进行升级!双向循环链表!当我们用户可能需要时常获取上一个数据元素时,即便使用单链表也能实现,但时间复杂度为O(n),由于时常进行这个操作,我们希望降降这个操作的复杂度,如何做到?改变结点多添加一个指针域,多了这个数据域的支持,在获取上一个元素时不用再遍历多个元素。
#include<iostream>
using namespace std;
template<typename T>
struct Node {
T data;
Node<T> *prior;
Node<T> *next;
};
template <typename T>
class BiLoopList {
Node<T> * tail;//抓主要矛盾 !!!!对链表所有操作都是通过尾指针进行的
int length;
public:
BiLoopList() {
tail = new Node<T>;
tail->prior = tail;
tail->next = tail;
length = 0;
}
BiLoopList(int n) {//尾插法
length = n;
tail = new Node<T>;
tail->next = tail;
tail->prior = tail;
Node <T> *p;
for (int i = 1; i <= n; i++) {
p = new Node<T>;
p->data = i;
p->next = tail->next;
p->prior = tail;
tail->next->prior = p;
tail->next = p;
tail = p;
}
}
bool get_value(int i, T & ele) {
if (0 < i <= length) {
Node <T> *p = tail->next;
for (int j = 1; j <= i; j++)
{
p = p->next;
}
ele = p->data;
return true;
}
else
return false;
}
bool insert(int i, const T & ele)
{
if (0 < i <=length+1) {
Node<T> *p = tail->next;
for (int k = 1; k < i; k++) {
p = p->next;
}
Node <T> *q = new Node<T>;
q->data = ele;
q->next = p->next;
q->prior = p;
p->next->prior = q;
p->next = q;
length++;
if (i == length)//天啊!!!完全因为上一句的length++ (这句话是后修改的 如果加入的元素是链表的最后一个 改变链表尾指针 后来想到这个问题 导致上下两个语句变量变化带来了bug) 以后做修改时先检查上下语句间相关变量的依赖性
tail = q;
return true;
}
else
return false;
}
bool del_ele(int i, T & ele) {
if (0 < i <= length) {
Node<T> *p = tail->next;
for (int j = 1; j <= i; j++) {
p = p->next;
}
p->prior->next = p->next;
p->next->prior = p->prior;
ele = p->data;
if (i == length)
tail = p->prior;
delete p;
length--;
return true;
}
else
return false;
}
int get_len() {
return length;
}
void show() {
Node<T> *p = tail->next->next;
//for (int i = 1; i < length; i++)
while(p->next!=tail->next)//好像明白为啥不需要length变量,与顺序表不同 必须要设置一个变量表示线性表长度 在这里通过指针信息(为空 循环链表特点)可以通过函数得到链表长度 省去一个变量空间 但是顺序表不能这样做 没有好的办法得到数组中有值的元素个数
{
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
};
int main()
{
BiLoopList<int > a(6);
int x;
if (a.get_value(6, x))
cout << x << endl;
if (a.insert(7, 66))
{
cout << a.get_len() << endl;
if (a.get_value(1, x))
cout << x << endl;
}
if(a.del_ele(7,x))
cout << x << endl;
if (a.get_value(6, x))
cout << x << endl;
a.show();
cin.get();
return 0;
}