数据结构篇——线性表之顺序表

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个元素,这时就可以很轻易的得到中位数了。

(图1)
(图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;
}

 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值