线性表是最基本、最简单、也是最常用的一种数据结构。它是由n个具有相同特性的数据元素的有限序列。其数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的。其主要的物理存储方式分为顺序表(相邻数据元素在底层结构上是连续的)和链表(一般是不连续的)。
顺序表实际上就是数组存储。为了使程序的应用范围更广,用类模板的方式将其进行封装。
首先先搭建类体,并利用C++运算符new和delete实现构造与析构。这部分中值得注意的是在拷贝构造时要采用深拷贝,避免浅拷贝造成的重复释放内存的问题。
#include
template <typename T>
class mylist
{
protected:
T *b, *e;
mylist(T *p, T *q) : b(p), e(q) {}
public:
mylist() : b(nullptr), e(nullptr) {}
~mylist()
{
if (b)
delete[] b;
}
mylist(T x, unsigned n)
{
if (n)
{
*(e = b = new T[n]) = x;
while (--n)
*++e = x;
++e;
}
}
mylist(std::initializer_list<T> x)
{
const T *p(x.begin());
*(e = b = new T[x.size()]) = *p;
while (++p < x.end())
*++e = *p;
++e;
}
mylist(const mylist &x) // 深拷贝
{
if (x.b == x.e)
b = e = nullptr;
else
{
T *p(x.b);
*(e = b = new T[x.e - x.b]) = *p;
while (++p < x.e)
*++e = *p;
++e;
}
}
mylist &operator=(const mylist &x)
{
if (x.b == x.e)
{
if (b != e)
delete[] b;
b = e = nullptr;
return *this;
}
T *p(x.b);
*(e = b = new T[x.e - x.b]) = *p;
while (++p < x.e)
*++e = *p;
++e;
return *this;
}
};
由于是类外函数,因此要在类内加入如下友元声明:
template <typename D>
friend bool listempty(const mylist<D> &);
直接判断数组指针是否为空即可,程序如下:
template <typename T>
inline bool listempty(const mylist<T> &x)
{
return x.b == x.e;
}
直接返回尾后指针和头指针之间的距离即可,程序如下:
template <typename T>
inline unsigned listlength(const mylist<T> &x)
{
return x.e - x.b;
}
从头指针开始遍历整个顺序表。程序如下:
#include
template <typename T>
void displist(const mylist<T> &x)
{
if (!x.b)
{
std::cout << "\t[空]\n";
return;
}
T *p(x.b);
unsigned i(1);
std::cout << "序号" << '\t' << "元素" << std::endl;
std::cout << i << '\t' << *p << std::endl;
while (++p < x.e)
std::cout << ++i << '\t' << *p << std::endl;
}
由于是顺序表,从头指针开始直接加上下标的值即可。程序如下:
template <typename T>
inline bool getelem(const mylist<T> &x, unsigned i, T &e)
{
if (!i || i > listlength(x))
return false;
e = x.b[i - 1];
return true;
}
出于后续程序的方便考虑,还重载了[]
运算符,但这是一个不安全的成员函数1。程序如下:
T &operator[](unsigned n)
{
return *(b + n);
}
由于数据未必有序,因此利用顺序查找:
template <typename T>
unsigned locateelem(const mylist<T> &x, T e)
{
if (!x.b)
return 0; // x为空
T *p(x.b);
do
if (*p == e)
return ++p - x.b;
while (++p < x.e);
return 0;
}
逐个“后移”,给“空”出来的位置赋值即可。程序如下:
template <typename T>
bool listinsert(mylist<T> &x, unsigned i, T e)
{ // 插入以后在第i+1个位置,因此允许i=0
if (i > listlength(x))
return false;
T *p(x.b), *q(x.e);
if (!i) // 在第1个位置插入
{
*(x.e = x.b = new T[q - p + 1]) = e;
if (p) // 有可能为空表
{
T *t(p);
do
*++x.e = *p;
while (++p < q);
delete[] t;
}
++x.e;
return true;
}
*(x.e = x.b = new T[q - p + 1]) = *p;
T *t(p);
while (--i)
*++x.e = *++p;
*++x.e = e;
while (++p < q)
*++x.e = *p;
delete[] t;
++x.e;
return true;
}
逐个“前移”,遇到待删除元素直接跳过,不拷贝即可。程序如下:
template <typename T>
bool listdelete(mylist<T> &x, unsigned i)
{
if (!i || i > listlength(x))
return false;
T *p(x.b), *q(x.e), *t(p);
if (i == 1)
{
if (q == ++p) // 只有一个元素
{
delete[] t;
x.e = x.b = nullptr;
return true;
}
*(x.e = x.b = new T[q - p]) = *p;
while (++p < q)
*++x.e = *p;
delete[] t;
++x.e;
return true;
}
*(x.e = x.b = new T[--q - p]) = *p;
--i;
while (--i)
*++x.e = *++p;
++p;
while (++p <= q)
*++x.e = *p;
++x.e;
delete[] t;
return true;
}
类似于析构:
template <typename T>
inline void listclear(mylist<T> &x)
{
if (x.b)
{
delete[] x.b;
x.b = x.e = nullptr;
}
}
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表的元素并不是单独的数据元素,而是由数据域和指针域构成的结构体。因此首先编写如下的结构体:
template <typename T>
struct myelem
{
T data;
myelem *next;
// 为了方便,写个构造函数
myelem(T a, myelem *p = nullptr) : data(a), next(p) {}
};
template <typename T>
class mylink
{
myelem<T> *head;
public:
mylink() : head(nullptr) {}
mylink(std::initializer_list<T> x)
{
const T *p(x.begin()), *q(x.end());
if (p != q)
{
myelem<T> *a(head = new myelem<T>(*p));
while (++p < q)
a = a->next = new myelem<T>(*p);
}
else
head = nullptr;
}
~mylink()
{
myelem<T> *p(head);
while (p)
{
p = p->next;
delete head;
head = p;
}
}
};
直接对头指针判断即可。程序如下:
template <typename T>
inline bool linkempty(const mylink<T> &x)
{
return !x.head;
}
利用循环及计数器直到指针域为空即可。程序如下:
template <typename T>
unsigned linklength(const mylink<T> &x)
{
const myelem<T> *p(x.head);
unsigned n(0);
if (p)
do
++n;
while (p = p->next);
return n;
}
为了与顺序表保持一致,因此写了普通函数而不是重载<<
运算符。程序如下:
template <typename T>
void displink(const mylink<T> &x)
{
const myelem<T> *p(x.head);
if (p)
{
unsigned n(0);
std::cout << "序号\t元素值" << std::endl;
do
std::cout << ++n << '\t' << p->data << std::endl;
while (p = p->next);
}
else
std::cout << "\t[空]" << std::endl;
}
由于链表以指针方式存储,物理结构上不相连,因此利用循环来得到元素值。程序如下:
template <typename T>
bool getelem(const mylink<T> &x, unsigned i, T &e)
{
if (!i)
return false;
const myelem<T> *p(x.head);
if (!p)
return false;
while (--i)
if (!(p = p->next))
return false;
if (p)
{
e = p->data;
return true;
}
return false;
}
同顺序表,顺序查找。程序如下:
template <typename T>
unsigned locateelem(const mylink<T> &x, T e)
{
const myelem<T> *p(x.head);
if (!p)
return 0;
if (p->data == e)
return 1;
unsigned k(2);
while (p = p->next)
{
if (p->data == e)
return k;
++k;
}
return 0;
}
先找位置,直接插入即可,不必再像顺序表一样逐个后移:
template <typename T>
bool linkinsert(mylink<T> &x, unsigned i, T e)
{
if (!i)
{
x.head = new myelem<T>(e, x.head);
return true;
}
myelem<T> *p(x.head);
if (!p)
return false;
while (--i)
if (!(p = p->next))
return false;
p->next = new myelem<T>(e, p->next);
return true;
}
同理,先找位置,再删除:
template <typename T>
bool linkdelete(mylink<T> &x, unsigned i)
{
if (!i)
return false;
myelem<T> *p(x.head);
if (!p)
return false;
while (--i)
if (!(p = p->next))
return false;
if (!p->next)
return false;
myelem<T> *q(p->next);
p->next = q->next;
delete q;
return true;
}
有了顺序表和链表两种类以后,来看看它们的应用。
给定 n n n个整数组成的序列 { N k } k = 1 n \{N_k\}_{k=1}^n {Nk}k=1n。“连续子列”被定义为 { N k } k = i j \{N_k\}_{k=i}^j {Nk}k=ij,其中 1 ⩽ i ⩽ j ⩽ n 1\leqslant i\leqslant j\leqslant n 1⩽i⩽j⩽n。“最大子列和”则被定义为所有连续子列元素的和中最大者。现要求计算给定整数序列的最大子列和。
遍历所有子列:
#include "list.h"
template <typename T>
T MaxSubseqSum(const mylist<T> &x)
{
T max(0), p, t;
for (unsigned i(1); i <= listlength(x); ++i)
for (unsigned j(i); j <= listlength(x); ++j)
{
p = 0;
for (unsigned k(i); k <= j; ++k)
{
getelem(x, k, t);
p += t;
}
if (p > max)
max = p;
}
return max;
}
时间复杂度: O ( n 3 ) O(n^3) O(n3)
由于最大子列是线性表中连续的序列,故可以将其转化为部分和序列的差的最大值问题。记
S k = { ∑ i = 1 k N i , 1 ⩽ k ⩽ n 0 , k = 0 S_k=\begin{cases} \sum\limits_{i=1}^kN_i,&1\leqslant k\leqslant n\\0,&k=0 \end{cases} Sk=⎩ ⎨ ⎧i=1∑kNi,0,1⩽k⩽nk=0
则问题转化为求 max i ⩾ j S i − S j \max\limits_{i\geqslant j}S_i-S_j i⩾jmaxSi−Sj。
#include "list.h"
template <typename T>
T MaxSubseqSum(const mylist<T> &x)
{
unsigned i(0);
T a(0), max(a), min(a), temp, result(a);
while (++i <= listlength(x))
{
getelem(x, i, temp);
if ((a += temp) > max)
{
if (result < (max = a) - min)
result = max - min;
}
else if (a < min)
min = a;
}
return result;
}
时间复杂度: O ( n ) O(n) O(n)
using namespace std;
int main()
{
mylist<int> x;
char ch;
unsigned a;
int b;
while (true)
{
cout << "请选择操作:\nA-判断线性表是否为空\nB-求线性表的长度\nC-输出线性表\nD-求线性表中某个元素的值\nE-按元素值查找\nF-插入数据元素\nG-删除数据元素\nH-求线性表的最大子列\nI-清空线性表\nJ-退出程序" << endl;
cin >> ch;
switch (ch)
{
case 'A':
if (listempty(x))
cout << "线性表为空!" << endl;
else
cout << "线性表非空!" << endl;
break;
case 'B':
cout << "线性表长度为" << listlength(x) << endl;
break;
case 'C':
cout << "线性表为:" << endl;
displist(x);
break;
case 'D':
cout << "请输入要获取的元素编号:";
cin >> a;
if (getelem(x, a, b))
cout << "获取成功!\n元素值为:" << b << endl;
else
cout << "获取失败!" << endl;
break;
case 'E':
cout << "请输入待查找的元素值:";
cin >> b;
if (a = locateelem(x, b))
cout << "查找成功!\n元素编号为:" << a << endl;
else
cout << "查找失败!" << endl;
break;
case 'F':
cout << "请输入要插入的位置编号:";
cin >> a;
cout << "请输入要插入的元素值:";
cin >> b;
if (listinsert(x, --a, b))
cout << "插入成功!" << endl;
else
cout << "插入失败!" << endl;
break;
case 'G':
cout << "请输入要删除的元素编号:";
cin >> a;
if (listdelete(x, a))
cout << "删除成功!" << endl;
else
cout << "删除失败!" << endl;
break;
case 'H':
cout << "最大子列和为:" << MaxSubseqSum(x) << endl;
break;
case 'I':
listclear(x);
cout << "清空成功!" << endl;
break;
case 'J':
return 0;
default:
cout << "选择错误!\n请重新选择!" << endl;
}
system("pause");
system("cls");
}
}
如测试线性表为:{6,8,-9,-7,4,0,8,-4,-5,-5}
输入H并回车即得:
“最大子列和为:14”
据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。则问题为,给定总人数以及报数k,一开始要站在什么地方才能避免被处决?
利用顺序表模拟逐个报数的过程。建立顺序表,第i个位置代表第i+1个人。程序如下:
#include "list.h"
unsigned josephus1(unsigned n, unsigned r)
{
if (!n)
return 0;
if (n == 1)
return 1;
mylist<bool> a(false, n);
unsigned i(((r - 1) % n)), k(r), m(n - 1);
a[i] = true;
while (--m)
{
do
if (a[++i %= n])
++k;
while (--k);
k = r;
a[i] = true;
}
return locateelem(a, false);
}
利用循环链表模拟逐个报数的过程。程序如下:
struct node
{
int no;
struct node *next;
};
unsigned josephus2(unsigned n, unsigned r)
{
unsigned i;
struct node *list, *p, *q;
(p = new node)->no = 1;
list = q = p;
for (i = 2; i <= n; ++i) // 创建链表
{
(p = new node)->no = i;
q->next = p, q = p; // else list->next=p,list=p;
}
q = p = q->next = list; // 首尾相连
while (p->next != p) // 进行排减
{
for (i = 1; i < r; ++i)
q = p, p = p->next; // 跳过m-1
q->next = p->next; // 排除m
delete p;
p = q->next; // 重新将链表相连
}
return p->no;
}
将n个人编号: 0 , 1 , ⋯ , n 0,\,1,\cdots,\,n 0,1,⋯,n。设报到 r r r的人退出,若一共有 n n n个人,设 a n ( r ) a_n^{(r)} an(r)表示此时剩下的人的编号,则每淘汰掉一个人,下一个人成为头,相当于把数组向前移动 r r r位。若已知 n − 1 n-1 n−1个人时,即胜利者的下标位置为 a n − 1 ( r ) a_{n-1}^{(r)} an−1(r),则 n n n个人的时候,就是往后移动 r r r位。因为有可能数组越界,超过的部分会被接到头上,所以还要模 n n n。故可得递推公式
a n ( r ) = { 0 , n = 1 ( a n − 1 ( r ) + r ) m o d n , n > 1 a_n^{(r)}=\begin{cases}0&,n=1\\(a_{n-1}^{(r)}+r)\bmod n&,n>1\end{cases} an(r)={0(an−1(r)+r)modn,n=1,n>1
但由于编号是从0开始的,故在最后需要再加一。程序如下:
unsigned sub_josephus(unsigned n, unsigned r)
{
if (n == 1)
return 0;
return (sub_josephus(n - 1, r) + r) % n;
}
unsigned josephus(unsigned n, unsigned r)
{
return sub_josephus(n, r) + 1;
}
int main()
{
unsigned n, r;
cin >> n >> r;
cout << josephus2(n, r);
system("pause");
return 0;
}
如:输入17 7,输出2。
无法判断该位置是否合法,即是否在申请的数组空间内。若使用不当,可能造成野指针问题。可能的解决方案是:另外建立一个静态数据成员error
,若非法,则返回error
的引用。但是一个弊端是无法为error
变量给定一个合适的值来表示非正常的情况,其原因是这是抽象类,无法确定一个统一的错误标志。若为具体的类型,则矛盾就可以迎刃而解。(如double
类就可以将error
赋值为NaN
) ↩︎