1、线性表
(1)定义
线性表是具有相同数据类型的n个数据元素的有限序列,其中n为表长,当n=0时,该线性表时是一个空表。若以L命名线性表,则其一般表示为
其中a1是唯一的“第一个”数据元素,又称为表头元素;an是唯一的“最后一个”元素,又称为表尾元素。除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。
(2)特点
-
表中元素的个数有限。(例:整数不是线性表)
-
表中元素具有逻辑上的顺序性,在序列中各元素排列有其先后顺序。(例:由n个实数组成的集合不是线性表)
-
表中元素元素都是数据元素,并且数据类型相同。
-
表中元素具有抽象性,仅关心元素间的逻辑关系,而不关心元素具体表示什么内容。
注意:线性表是一种逻辑结构,它从逻辑关系上描述数据,与数据的具体存储无关。顺序表和链表是存储结构,它们与线性表是不同层面的概念。
2、顺序表
(1)定义
线性表的顺序存储又称为顺序表,它是用一组地址连续的储存单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理上也相邻。
(2)特点
-
顺序表中元素的下标从0开始。(线性表必须从1开始)
-
顺序表可以实现随机存取(访问)。
(3)基本操作
① 建表:创建一个顺序表来表示线性表。
const int MAX_LEN = 1000; //最大长度
typedef int ElemType; //元素类型
typedef struct { //链表结构体
ElemType data[MAX_LEN]; //数据
int len; //长度
} SeqList;
//建表
SeqList createSeqList(ElemType *a = NULL, int len = 0) {
SeqList L;
L.len = len;
for (int i = 0; i < len; i++) {
L.data[i] = a[i];
}
return L;
}
② 插入:往线性表中指定位置pos插入数据元素e。其中pos的有效位置是1至len + 1。(注意:最后L.len需要执行“++”操作)
//插入 T = O(n), S = O(1)
bool insert(SeqList& L, int pos, ElemType e) {
//违规操作
if (pos <= 0 || pos > L.len + 1 || L.len >= MAX_LEN) {
return false;
}
//正常插入(1至L.len+1)
for (int i = L.len; i >= pos; i--) {
L.data[i] = L.data[i - 1];
}
L.data[pos - 1] = e;
L.len++;
return true;
}
③ 删除:删除线性表中指定位置pos的元素,将其赋值给e。其中pos的有效位置是1至len。(注意:最后L.len需要执行“--”操作)
//删除 T = O(n), S = O(1)
bool del(SeqList& L, int pos, ElemType& e) {
//违规操作
if (pos <= 0 || pos > L.len) {
return false;
}
//正常删除(1至L.len)
e = L.data[pos - 1];
for (int i = pos - 1; i < L.len - 1; i++) {
L.data[i] = L.data[i + 1];
}
L.len--;
return true;
}
④ 按值删除:删除线性表中所有值为value的元素。
//按值删除——删除所有值为value的节点
//T = O(n), S = O(1)
void delByValue(SeqList& L, ElemType value) {
int newLen = 0; //删除后顺序表的长度
for (int i = 0; i < L.len; i++) {
if (L.data[i] != value) {
L.data[newLen++] = L.data[i];
}
}
L.len = newLen;
}
⑤ 按值查找:在线性表中找到第一个value元素的下标,查找失败时返回0。
//按值查找
//T = O(n), S = O(1)
int findByValue(SeqList L, ElemType value) {
for (int i = 0; i < L.len; i++) {
if (L.data[i] == value) {
return i + 1;
}
}
return 0;
}
⑥ 翻转:翻转线性表[begin, end)之间的元素。
//翻转顺序表,翻转区间——[begin, end)
//T = O(n), S = O(1)
void reverse(SeqList& L, int begin, int end) {
int low = begin, high = end - 1;
for (; low < high; low++, high--) {
//交换
ElemType temp = L.data[low];
L.data[low] = L.data[high];
L.data[high] = temp;
}
}
⑦ 合并:合并两个有序的线性表,让其合并的结果仍然有序。
//合并两个有序顺序表 T = O(m + n), S = O(1)
bool merge(SeqList La, SeqList Lb, SeqList& Lc) {
//超长
if (La.len + Lb.len > MAX_LEN) {
return false;
}
//正常合并
int i = 0, j = 0, k = 0;
//两个表均未遍历完
while (i < La.len && j < Lb.len) {
Lc.data[k++] = (La.data[i] <= Lb.data[j] ? La.data[i++] : Lb.data[j++]);
}
//拷贝剩下的
while (i < La.len) {
Lc.data[k++] = La.data[i++];
}
while (j < Lb.len) {
Lc.data[k++] = Lb.data[j++];
}
//修改长度
Lc.len = La.len + Lb.len;
return true;
}
3、相关习题
(1)顺序表的翻转(变形)
① 已知一长度为m+n的线性表依次由这样两个线性表
组成,请编写一个函数,将b换到a前
算法思想:如下图,先将线性表L1翻转成为L2,再对L2分别在区间[0, n)和[n, m + n )进行翻转,即可得到L3。
代码实现:
//交换线性表的a, b两部分 T = O(m + n), S = O(1)
void exchange(SeqList& L, int m, int n) {
//整体翻转
reverse(L, 0, L.len);
//部分翻转
reverse(L, 0, n);
reverse(L, n, m + n);
}
② 编写一个函数,实现对线性表的循环左移p位。如下图,线性表L1循环左移2位后变成线性表L3。
算法思想:如下图,先对顺序表L1分别在区间[0, 2)和[2, 6)(即[0, p)和[p, len))进行翻转得到L2,再对L2整体进行翻转得到L3,这样便达到了循环左移2(p)位的目的。
代码实现:
//循环左移p位 T = O(n), S = O(1)
void rsl(SeqList& L, int p) {
//对[0, p)和[p, len)部分分别翻转
reverse(L, 0, p);
reverse(L, p, L.len);
//整体翻转
reverse(L, 0, L.len);
}
(2)顺序表的合并(变形)
一个长度为n的升序线性表的中位数为第⌈n/2⌉个数据元素。例如,线性表(1, 2, 3)的中位数为2,(1, 2, 3, 4)的中位数为2。现给定两个长度相等的升序线性表La和Lb,给出算法思想并编写相关代码求出La和Lb合并以后的中位数,要求做到时间复杂度T ≤ O(logn),空间复杂度S = O(1)。
算法思想:
分别求两个升序线性表La, Lb的中位数,设为a和b,求它们合并后的线性表Lc的中位数过程如下:
I. 若a=b,则a或b就是所要求的中位数,算法结束;
II. 若a < b,则删除La中较小的一半序列,同时舍弃B中较大的一半序列,要求两次舍弃的长度相等;
III. 若a > b,则舍弃La中较大的一半序列,同时舍弃Lb中较小的一半序列,要两次舍弃的长度相等。
在保留的两个升序序列中,重复上述3个步骤,直到两个序列均只含一个元素为止。
算法理解:
I. 算法中a=b的情况很好理解,这时La的前一半和Lb的前一半刚好构成Lc的前一半,所以a或b就是中位数。
II. a < b的情况中,如果La和Lb都只有一个元素,那就可以立刻找到中位数。所以如果我们可以通过不断删除顺序表大部分元素,最终让La和Lb都只含有一个元素,那就能快速求出来。
III. 删除的过程如下图。如下图1,La分为橘色和绿色两部分;Lb分为蓝色和灰色两部分。对La和Lb合并之后得到Lc,因为a < b即La.data[mid1] < Lb.data[mid2],所以可知,mid1这个元素在Lc的位置应该是在Lc的mid之前的,mid2则在之后,所以可以画出Lc大致的图。如下图2,删除La中mid1之前的元素事实上就是删除Lc中mid1之前的橘色元素。于是,通过删除Lc中的mid1之前的橘色元素和mid2之后的灰色元素就可以缩短Lc,终有一天,Lc会缩短到只有2个元素,这时就可以很轻易的得到中位数了。
代码实现:
//找【两个长度相同】的有序顺序表合并以后的中位数
//中位数:长度为n的表中位数——n/2向上取整
//T = O(logn), S = O(1)
ElemType findMergeMid(SeqList La, SeqList Lb) {
//长度不相等,该算法失效
if (La.len != Lb.len) {
return ERROR;
}
int s1 = 0, e1 = La.len - 1, m1; //La的表头元素,表尾元素,中位数索引
int s2 = 0, e2 = Lb.len - 1, m2; //Lb的表头元素,表尾元素,中位数索引
//还有元素时不能停止
while (s1 != e1 || s2 != e2) {
m1 = (s1 + e1) / 2;
m2 = (s2 + e2) / 2;
//开始判断和进行删除
if (La.data[m1] == Lb.data[m2]) { //满足条件1
return La.data[m1];
}
else if(La.data[m1] < Lb.data[m2]) { //满足条件2
//舍弃La前半部分,需确保它和Lb舍弃一样多的元素
s1 = ((s1 + e2) % 2 == 0 ? m1 : m1 + 1);
//舍弃Lb的后半部分
e2 = m2;
}
else { //满足条件3
e1 = m1;
s2 = ((s2 + e2) % 2 == 0 ? m2 : m2 + 1);
}
}
//s1和s2所指的元素刚好是中间那两个,返回小的
return La.data[s1] < Lb.data[s2] ? La.data[s1] : Lb.data[s2];
}
(3)其他
① 一个长度为n的线性表,如果其中某个元素出现了超过n/2次,则称这个元素为这个线性表的主元素。例如,线性表(0, 3, 0, 0, 2)的主元素为0,线性表(1, 2, 3)没有主元素。编写一个函数,判断给定的顺序表是否含有主元素,如果有,则将其返回,否则返回一个标识ERROR(可设ERROR为一个超大的数)。要求做到时间复杂度T ≤ O(n),空间复杂度S = O(1)。
算法思想:
假设顺序表中一定含有主元素mainEle。那么顺序表中所有的mainEle元素的总数已经占了线性表的一半以上,如果每一个mainEle元素和一个其他元素进行对消,那么起码至少还剩1个mainEle。所以可以通过对消的方法来获取最后剩下的那个元素,那个就是主元素。
对消规则的为:依次扫描所给数组中的每一个整数,将第一个遇到的整数保存到mainEle中,并且将计数器count的值置为1;若遇到的下一个整数仍等于mainEle,则计数器加1,否则计数器减1;当计数器减到0时,将下一个整数再次保存到mainEle中,计数器再次置为1,开始新一轮的计数。然后继续遍历,直到遍历结束。
还有一个问题,顺序表中可能压根没有主元素,所以需要对上述找到的主元素进行判断,判断它是否是真的主元素。再次遍历顺序表,统计mainEle出现的次数可知它是否是真的主元素。
代码实现:
//找主元素 T = O(n), S = O(1)
ElemType findMainEle(SeqList L) {
ElemType mainEle = L.data[0]; //主元素
int count = 1; //计数器
//更新主元素和计数器
for (int i = 1; i < L.len; i++) {
//更新主元素的票数
count = (L.data[i] == mainEle ? count + 1 : count - 1);
//如果计数器出现负数,主元素要更新
if (count < 0) {
mainEle = L.data[i];
count = 1;
}
}
//判断是否是真的主元素
count = 0;
for (int i = 0; i < L.len; i++) {
if (L.data[i] == mainEle) {
count++;
}
}
return (count > L.len / 2 ? mainEle : ERROR);
}
② 给定一个顺序表,它的元素时整数类型,请编写一个函数,找出顺序表中未出现的最小正整数。例如(2, 3, 4, -1)中未出现的最小正整数为1,(1, 2, 3)中未出现的最小正整数为4。要求时间复杂度T ≤ O(n)。
算法思想:
首先理解这样一个事实,长度为n的整型顺序表,它未出现的最小正整数一定落在[1, n + 1]之中。可以这样理解,顺序表一开始没有元素,此时未出现的最小正整数nApeMin是1。然后往顺序表中加入1个数据,这个数如果是1,那么nApeMin就要改为2,;如果这个数据不是1,那么nApeMin根本不用动。所以加入数据n次,nApeMin最多挪动n次,也就是nApeMin最大也不超过n+1。
根据nApeMin的范围,可以开辟一个长度为n + 1的标志数组,初始时它全为0。然后遍历顺序表,若表中元素落在[1, n + 1]之间,则将相应的数组标志置为1。最后遍历标志数组,找到标志数组中第一个值为0的下标,这就是未出现的最小正整数。
代码实现:
//找最小正整数 T = O(n), S = O(n)
ElemType findNApeMinPst(SeqList L) {
//初始化
bool* exists = new bool[L.len + 1];
for (int i = 0; i < L.len + 1; i++) {
exists[i] = false;
}
//修改标志数组
for (int i = 0; i < L.len; i++) {
if (L.data[i] >= 1 && L.data[i] <= L.len + 1) {
exists[L.data[i] - 1] = true;
}
}
//找结果
for (int i = 0; i < L.len + 1; i++) {
if (exists[i] == false) {
delete[] exists; //释放内存
return i + 1;
}
}
}
所有源代码:
#include <iostream>
using namespace std;
/*------------------顺序表及其基本操作------------------*/
const int MAX_LEN = 1000; //最大长度
typedef int ElemType; //元素类型
typedef struct { //链表结构体
ElemType data[MAX_LEN]; //数据
int len; //长度
} SeqList;
const ElemType ERROR = 2147483647; //错误信息
//建表
SeqList createSeqList(ElemType *a = NULL, int len = 0) {
SeqList L;
L.len = len;
for (int i = 0; i < len; i++) {
L.data[i] = a[i];
}
return L;
}
//插入 T = O(n), S = O(1)
bool insert(SeqList& L, int pos, ElemType e) {
//违规操作
if (pos <= 0 || pos > L.len + 1 || L.len >= MAX_LEN) {
return false;
}
//正常插入(1至L.len+1)
for (int i = L.len; i >= pos; i--) {
L.data[i] = L.data[i - 1];
}
L.data[pos - 1] = e;
L.len++;
return true;
}
//删除 T = O(n), S = O(1)
bool del(SeqList& L, int pos, ElemType& e) {
//违规操作
if (pos <= 0 || pos > L.len) {
return false;
}
//正常删除(1至L.len)
e = L.data[pos - 1];
for (int i = pos - 1; i < L.len - 1; i++) {
L.data[i] = L.data[i + 1];
}
L.len--;
return true;
}
//按值删除——删除所有值为value的节点
//T = O(n), S = O(1)
void delByValue(SeqList& L, ElemType value) {
int newLen = 0; //删除后顺序表的长度
for (int i = 0; i < L.len; i++) {
if (L.data[i] != value) {
L.data[newLen++] = L.data[i];
}
}
L.len = newLen;
}
//按值查找
//T = O(n), S = O(1)
int findByValue(SeqList L, ElemType value) {
for (int i = 0; i < L.len; i++) {
if (L.data[i] == value) {
return i + 1;
}
}
return 0;
}
//翻转顺序表,翻转区间——[begin, end)
//T = O(n), S = O(1)
void reverse(SeqList& L, int begin, int end) {
int low = begin, high = end - 1;
for (; low < high; low++, high--) {
//交换
ElemType temp = L.data[low];
L.data[low] = L.data[high];
L.data[high] = temp;
}
}
//合并两个有序顺序表 T = O(m + n), S = O(1)
bool merge(SeqList La, SeqList Lb, SeqList& Lc) {
//超长
if (La.len + Lb.len > MAX_LEN) {
return false;
}
//正常合并
int i = 0, j = 0, k = 0;
//两个表均未遍历完
while (i < La.len && j < Lb.len) {
Lc.data[k++] = (La.data[i] <= Lb.data[j] ? La.data[i++] : Lb.data[j++]);
}
//拷贝剩下的
while (i < La.len) {
Lc.data[k++] = La.data[i++];
}
while (j < Lb.len) {
Lc.data[k++] = Lb.data[j++];
}
//修改长度
Lc.len = La.len + Lb.len;
return true;
}
//输出
void display(SeqList L) {
cout << "len = " << L.len << ", data = ( ";
for (int i = 0; i < L.len; i++) {
cout << L.data[i] << (i == L.len - 1 ? " )\n" : ", ");
}
}
/*------------------习题------------------*/
//交换线性表的a, b两部分 T = O(m + n), S = O(1)
void exchange(SeqList& L, int m, int n) {
//整体翻转
reverse(L, 0, L.len);
//部分翻转
reverse(L, 0, n);
reverse(L, n, m + n);
}
//循环左移p位 T = O(n), S = O(1)
void rsl(SeqList& L, int p) {
//对[0, p)和[p, len)部分分别翻转
reverse(L, 0, p);
reverse(L, p, L.len);
//整体翻转
reverse(L, 0, L.len);
}
//找【两个长度相同】的有序顺序表合并以后的中位数
//中位数:长度为n的表中位数——n/2向上取整
//T = O(logn), S = O(1)
ElemType findMergeMid(SeqList La, SeqList Lb) {
//长度不相等,该算法失效
if (La.len != Lb.len) {
return ERROR;
}
int s1 = 0, e1 = La.len - 1, m1; //La的表头元素,表尾元素,中位数索引
int s2 = 0, e2 = Lb.len - 1, m2; //Lb的表头元素,表尾元素,中位数索引
//还有元素时不能停止
while (s1 != e1 || s2 != e2) {
m1 = (s1 + e1) / 2;
m2 = (s2 + e2) / 2;
//开始判断和进行删除
if (La.data[m1] == Lb.data[m2]) { //满足条件1
return La.data[m1];
}
else if(La.data[m1] < Lb.data[m2]) { //满足条件2
//舍弃La前半部分,需确保它和Lb舍弃一样多的元素
s1 = ((s1 + e2) % 2 == 0 ? m1 : m1 + 1);
//舍弃Lb的后半部分
e2 = m2;
}
else { //满足条件3
e1 = m1;
s2 = ((s2 + e2) % 2 == 0 ? m2 : m2 + 1);
}
}
//s1和s2所指的元素刚好是中间那两个,返回小的
return La.data[s1] < Lb.data[s2] ? La.data[s1] : Lb.data[s2];
}
//找主元素 T = O(n), S = O(1)
ElemType findMainEle(SeqList L) {
ElemType mainEle = L.data[0]; //主元素
int count = 1; //计数器
//更新主元素和计数器
for (int i = 1; i < L.len; i++) {
//更新主元素的票数
count = (L.data[i] == mainEle ? count + 1 : count - 1);
//如果计数器出现负数,主元素要更新
if (count < 0) {
mainEle = L.data[i];
count = 1;
}
}
//判断是否是真的主元素
count = 0;
for (int i = 0; i < L.len; i++) {
if (L.data[i] == mainEle) {
count++;
}
}
return (count > L.len / 2 ? mainEle : ERROR);
}
//找未出现的最小正整数 T = O(n), S = O(n)
ElemType findNApeMinPst(SeqList L) {
//初始化
bool* exists = new bool[L.len + 1];
for (int i = 0; i < L.len + 1; i++) {
exists[i] = false;
}
//修改标志数组
for (int i = 0; i < L.len; i++) {
if (L.data[i] >= 1 && L.data[i] <= L.len + 1) {
exists[L.data[i] - 1] = true;
}
}
//找结果
for (int i = 0; i < L.len + 1; i++) {
if (exists[i] == false) {
delete[] exists; //释放内存
return i + 1;
}
}
}
//测试顺序表的操作
void testSeqLOp() {
//建表
ElemType LData[4] = { 2, -3, 0, -3 };
SeqList L = createSeqList(LData, 4);
cout << "原始数据:" << endl;
display(L);
//插入
int pos = 2; ElemType e = 2;
insert(L, pos, e);
cout << endl << "在第" << pos << "个位置插入元素[" << e << "]后:" << endl;
display(L);
//删除
pos = 1;
del(L, pos, e);
cout << endl << "删除第" << pos << "个元素[" << e << "]后:" << endl;
display(L);
//按值删除
e = -3;
delByValue(L, e);
cout << endl << "删除所有值为[" << e << "]的节点后:" << endl;
display(L);
//按值查找
e = 0;
pos = findByValue(L, e);
cout << endl << "查找元素[" << e << "]的位置:" << pos << endl;
//链表翻转
cout << endl << "翻转前:" << endl; display(L);
reverse(L, 0, L.len);
cout << "翻转链表:" << endl; display(L);
//有序链表合并
ElemType LaData[] = { -2, 3, 5, 9 };
SeqList La = createSeqList(LaData, 4);
ElemType LbData[] = { -1, 0, 2, 4 };
SeqList Lb = createSeqList(LbData, 4);
SeqList Lc;
merge(La, Lb, Lc);
cout << endl << "有序链表合并:" << endl;
cout << "La: "; display(La);
cout << "Lb: "; display(Lb);
cout << "Lc: "; display(Lc);
}
//习题测试
void testExercise() {
//交换线性表两部分
ElemType LData0[] = { -2, 3, 1, 0, 2, 7, 3, 5 }, m = 3, n = 5;
SeqList L = createSeqList(LData0, 8);
cout << "交换前:" << endl;
cout << "L: "; display(L);
exchange(L, m, n);
cout << "交换后:" << endl;
cout << "L: "; display(L);
//循环左移
int p = 2;
cout << endl << "循环左移前:" << endl;
cout << "L: "; display(L);
rsl(L, p);
cout << "循环左移" << p << "位后:" << endl;
cout << "L: "; display(L);
//找中位数
ElemType LaData[] = { -2, 3, 5, 9 };
SeqList La = createSeqList(LaData, 4);
ElemType LbData[] = { -1, 0, 2, 4 };
SeqList Lb = createSeqList(LbData, 4);
ElemType midValue = findMergeMid(La, Lb);
cout << endl << "两个长度相同的有序顺序表合并以后的中位数:" << endl;
cout << "La: "; display(La);
cout << "Lb: "; display(Lb);
cout << "中位数:" << midValue << endl;
//找主元素
ElemType LData1[] = { 2, 2, 3, 4, 2, 2, 4 };
L = createSeqList(LData1, 7);
cout << endl << "L:"; display(L);
cout << "主元素为:" << findMainEle(L) << endl;
//找未出现的最小正整数
ElemType LData2[] = { 2, 1, 3, 5, 4 };
L = createSeqList(LData2, 5);
cout << endl << "L:"; display(L);
cout << "未出现的最小正整数为:" << findNApeMinPst(L) << endl;
}
//测试
int main() {
cout << "---------------------顺序表的基本操作---------------------" << endl;
testSeqLOp();
cout << endl << endl << endl
<< "---------------------顺序表的相关习题---------------------" << endl;
testExercise();
return 0;
}