线性表

1.顺序存储——顺序表

2.链式存储

​ 2.1 单链表

​ 2.2 双链表

​ 2.3 循环链表

​ 2.4 静态链表——借助数组实现

1.线性表

1.概念

1.具有相同数据类型的n个数据元素的有限序列,其中n为表长,当n = 0时线性表是一个空表。

2.特点:

1)每个数据元素所占空间一样大

2)有次序

3)除第一个元素外,每个元素有且仅有一个直接前驱;除了最后一个元素外,每个元素有且仅有一个直接后继

4)位序:线性表中的第i个元素,其中i即为位序,位序从1开始

2.基本操作

//初始化,构造空的线性表,分配内存空间
InitList(&L);
//销毁操作,释放内存空间
DestoryLit(&L);
//插入删除
ListInsert(&L,i,e);
ListDelete(&L,i,&e);
//按值查找
LocateElem(L,e);
//按位序查找
GetElem(L,i);
//表长、打印链表、判空
Length(L);
PrintList(L);
Empty(L);

1.注意:对数的操作,——创销,增删改查

2.为什么要实现基本操作?

团队合作编程,封装思想,方便后续使用。

2.顺序表

1.概念与特点

1、顺序表——用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。

2、特点

1)随机访问

2)存储密度高

3)拓展容量不方便

4)插入删除元素不方便

2.实现方式——静态分配

#define MaxSize 10
typedef struct{
  ElemType data[MaxSize];
  int lenght;
}SeqList;

1、在分配内存空间的时候,一定要进行初始化,否则内存中遗留的脏数据会导致一些很隐蔽的bug;

2、静态分配方式:定义一个静态数组存放顺序表的数据;

3、缺点:顺序表的大小固定,分配后无法更改;如果一开始就声明一个很大的内存空间,在使用过程中,仅仅用到了很小的一部分,则内存的利用率很低

3.实现方式——动态分配

#define InitSize 100
typedef struct{
  ElemType *data;   //指向动态分配数组的指针
  int lenght;
  int capacity;     //顺序表的最大容量
}SeqList;

//初始化
void InitList(&L){
  L.data = (ElemType*)malloc(sizeof(ElemType)*InitSize);
  L.length = 0;
  L.capacity = InitSize;
}

//增加动态数组的长度
void IncreaseSize(SeqList &L, int len){
  int *p = L.data;
  L.data = (int *)malloc(L.capacity + len)*sizeof(int));
  for(int i = 0; i < L.length; ++i){
    L.data[i] = p[i];
  }
  L.capacity = L.capacity + len;
  free(p);
}

4.插入操作

1、复杂度

时间复杂度:最好O(1);最坏O(n);平均O(n/2)

//在L的位序i出插入元素e
bool ListInsert(SeqList& L, int i , int e){
  if(i < 1 || i > L.length + 1){
    assert("插入位置非法");
    return false;
  }
  if(i >= L.capacity){
    assert("存储空间满,不能插入");
    return false;
  }
  L.length++;            //前面的判断已经说明插入位置合法
  for(int j = L.length - 1; j >= i - 1; --j){
    L.data[j + 1] = L.data[j];
  }
  L.data[i - 1] = e;
  return true;
}

2、注意

1)L.length 除了表示顺序表的长度外,另外还表示顺序表中的最后一个元素的索引,不能是位序

2)L.legth++写在for循环前,还是后,均正确;

如果写在for后面,如下当length = 0时,不会进入循环,直接执行L.data[i - 1] = e 和 L.length++;

L.data[i - 1] = e;
L.length++;

如果写在for前面,代码则会更加直观,易于阅读,个人感觉

5.删除操作

1.复杂度

最好情况O(1);最坏情况O(n);平均情况O((n - 1)/2)

bool ListDelete(SeqList &L, int i, int &e){
  if(i < 1 || i > L.length){
    assert("删除位置非法")
    return false;
  }
  e = L.data[i - 1];
  //向前移动
  for(int j = i; j < L.length; ++j){
    L.data[j - 1] = L.data[j];
  }
  L.lenght--;
  return true;
}

注意要删除的是第i个元素,还是数组下标为i的元素

6.查找操作

1.按位查找——GetElem

O(1)

//i表示位序,查找第i个位置的元素,通过e返回
bool GetElem(SeqList L, int i, int& e){
  if(i < 1 || i > L.length){
    assert("访问位置非法");
    return false;
  }
  e = L.data[i - 1];
  return true;
}

2.按值查找——LocateElem

复杂度:最好O(1);最坏O(n);平均O(n/2)

//查找顺序表中第一个元素值等于val的元素,并返回其位序
bool LocateElem(SeqList L, int val, int index){
  for(int i = 0; i < L.length; i++){
    if(L.data[i] == val){
      index = i + 1;
      return true;
    }
  }
  index = -1;
  return false;
}

注意:此查找操作,只能用于查找基本类型,不能用于查找结构体类型,C++可以通过重载运算符判断结构类型是否相等

3.单链表——链表

1.概念和特点

1.定义:每个结点除了存放数据元素外,还要存储指向下一个结点的指针。

2.优点:不要求大片连续空间,改变容量方便

3.缺点:不可随机存取,要耗费一定空间存放指针

2.代码定义单链表:

typedef struct LNode{
  ElemType data;
  struct LNode *next;
}LNode,*LinkList;

3.不带头结点

//初始化
bool InitList(LinkList &L){
  L = NULL;
  return true;
}

//判断单链表是否为空
bool Empty(LinkList L){
  if(L == NULL){
    return true;
  }
  else{
    return false;
  }
}

4.带头结点

//初始化
bool InitList(LinkList &L){
  L = (LNode*)malloc(sizeof(LNode));
  if(L == NULL)
    return false;
  L->next = NULL;     //头结点之后暂时没有节点
  return true;
}

//判断带头结点的单链表是否为空
bool Empty(LinkList L){
  if(L->next == NULL)
    return true;
  else
    return false;
}

注意:带头结点的代码写代码更方便

5.单链表的插入

1.按位序插入节点

最好O(1);最坏o(n);

//带头结点,在第i个位置插入元素e
//此处的L指向头结点
bool ListInsert(LinkList &L, int i, ElemType e){
  if(i < 1){
    assert("插入位序非法");
    return false;
  }
  LNode *p;            //找到要插入位置的前一个节点的位置
  int j = 0;
  p = L;   //L指向头结点,头结点是第0个节点
  while(p != NULL && j < i - 1){
    p = p->next;
    j++;
  }
  if(p == NULL){            //第i-1 个节点不存在,插入位置非法
    assert("插入位序非法");
    return false;
  }
  LNode* s = (LNode*)malloc(sizeof(LNode));
  s->data = e;
  s->next = p->next;
  p->next = s;
  return true;
}
//不带头结点;在第i个位置插入元素e
//区别在于带头结点的对于第1个位置的插入需要特殊讨论
//此时的L表示第一个节点,已经不是头结点了,代码需要注意
bool ListInsert(LinkList &L, int i, ElemType e){
  if(i < 1){
    assert("插入位序非法");
    return false;
  }
  //注意,此时的L表示第一个节点,已经不是头结点了
  if(i == 1){
    LNode* s = (LNode*)malloc(sizeof(LNode));
    s->data = e;
    s->next = L;
    L = s;
    return true;
  }

  LNode *p;            //找到要插入位置的前一个节点的位置
  int j = 1;    //0处需要特殊讨论,此处从1开始

  p = L;   
  while(p != NULL && j < i - 1){
    p = p->next;
    j++;
  }
  if(p == NULL){            //第i-1 个节点不存在,插入位置非法
    assert("插入位序非法");
    return false;
  }
  LNode* s = (LNode*)malloc(sizeof(LNode));
  s->data = e;
  s->next = p->next;
  p->next = s;
  return true;
}

2.指定节点的后插操作

//在p节点后插入元素e
bool InsertNextNode(LNode *p, ElemType e){
  if(p == NULL)
    return false;
  LNode* s = (LNode*)malloc(sizeof(LNode));
  if(s == NULL){
    return false;
  }
  s->data = e;
  s->next = p->next;
  p->next = s;
  return true;
}

3.指定节点的前插操作

//在p节点之前插入元素e
bool InsertPriorNode(LNode *p, ElemType e){
  if(p == NULL){
    return false;
  }
  LNode *s = (LNode*)malloc(sizeof(LNode));
  s->data = e;
  //先后插
  s->next = p->next;
  p->next = s;
  //再交换数据
  swap(s->data, p->data);
  return true;
}

6.单链表的删除

此处指讨论带头结点的(不带头结点的链表删除时需要考虑第0个节点)

1.删除第i个位置的节点

O(n)

//删除第i个位置的节点
bool ListDelete(LinkList &L, int i, ElemType &e){
  if(i < 1){
    assert("删除位置非法");
    return false;
  }
  LNode *p;        //p指向要删除位置的前一个节点,第i - 1个节点
  int j = 0;
  p = L;        //L指向头结点
  while(p != NULL && j < i - 1){
    p = p->next;
    j++;
  }
  //要删除位置的前一个节点为空,删除位置非法
  if(p == NULL){
    return false;
  }
  //要删除位置的节点不存在,i - 1节点不为空
  if(p->next == NULL){
    assert("第 i - 1个节点之后已无其他节点")
    return false;
  }
  //删除第i个节点
  LNode *q = p->next;
  e = q->data;
  p->next = q->next;
  free(q);
  return true;
}

2.删除指定节点

//删除指定节点p
bool DeleteNode(LNode* L,LNode *p){
  if(p == NULL){
    return false;
  }
  LNode *q = p->next;
  //如果是最后一个节点,则会报错
  if(q == NULL){
    q = L->next;
    while(q != NULL && q->next != p){
      q = q->next;
    }
    if(q == NULL){
      assert("删除非法,未找到p");
      return false;
    }
    q->next = NULL;
    free(p);
    return true;
  }
  //p不是最后一个节点
  p->data = p->next->data;
  p->next = q->next;
  free(q);
  return true;;
}

7.单链表的查找

O(n)

1.按位查找

//按位查找,查找第i个节点,带头结点
LNode * GetElem(LinkList L, int i){
  if(i < 0){
    return NULL;
  }
  LNode *p;
  int j = 0;   //有时候会用到查找头结点的情况,因此从0开始好
  p = L;
  while(p != NULL && j < i){
    p = p->next;
    j++;
  }
  return p;
}

实现查找后,在第i个位置插入元素e可以简化为:

//带头结点,在第i个位置插入元素e
//此处的L指向头结点
bool ListInsert(LinkList &L, int i, ElemType e){
  if(i < 1){
    return false;
  }
  //先查找第i - 1个结点
  LNode* p = GetElem(L, i - 1);
  if(p == NULL){
    assert("插入位置非法");
    return false;
  }
  //在第p个节点后插
  if(!InsertNextNode(p, e)){
    assert("插入失败");
    return false;
  }
  return true;
}

同样的道理,可以简化删除第i个节点,先找到第i-1个节点,在进行删除

//删除第i个位置的节点
bool ListDelete(LinkList &L, int i, ElemType &e){
  if(i < 1){
    assert("删除位置非法");
    return false;
  }
  LNode *p = GetElem(L, i - 1);

  //要删除位置的前一个节点为空,删除位置非法
  if(p == NULL){
    return false;
  }
  //要删除位置的节点不存在,i - 1节点不为空
  if(p->next == NULL){
    assert("第 i - 1个节点之后已无其他节点")
    return false;
  }
  //删除第i个节点
  LNode *q = p->next;
  e = q->data;
  p->next = q->next;
  free(q);
  return true;
}

2.按值查找

//按值查找,找到数据==e的节点
LNode* LocateElem(LinkList L, ElemType e){
  LNode *p = L->next;
  while(p != NULL && p->data != e){
    p = p->next;
  }
  //找到返回p,找不到返回NULL
  return p;
}

注意,查找只能查找基本类型,对于结构类型,需要重载运算符

8.求表长

O(n)

int Length(LinkList L){
  int len = 0;
  LNode *p = L;
  while(p->next != NULL){
    p = p->next;
    len++;
  }
  return len;
}

9.尾插法——建立单链表

O(1)

//尾插元素过多时,每次插入都需要遍历列表,因此可以设置一个尾指针
LinkList L;
LinkList tail;
InitList(L);        //初始化
tail = L;
//带头结点
bool List_TailInsert(LinkList &tail, ElemType e){
  LNode * p = (LNode*)malloc(sizeof(LNode));
  if(p == NULL){
    return false;
  }
  p->data = e;
  p->next = NULL;

  tail->next = p;
  tail = p;
  return true;
}

10.头插法——建立单链表

//头插法,带头结点
bool List_HeadInsert(LinkList &L, ElemType e){
  LNode* s = (LNode*)malloc(sizeof(LNode));
  if(s == NULL){
    return false;
  }
  s->data = e;
  s->next = L->next;
  L->next = s;
  return true;
}

4.双链表——链表

1.概念和特点

1.单链表缺点是无法进行逆检索,有时候不太方便

2.双链表,可进可退,存储密度更低一丢丢

2.代码定义双链表

typedef struct DNode{
  ElemType data;
  struct DNode *prior,*next;
}DNode,*DLinkList;

3.初始化双链表

//带头结点的双链表
bool InitDLinkList(DLinkList &L){
  L = (DNode*)malloc(sizeof(DNode));
  if(L == NULL)
    return false;
  L->prior = NULL;
  L->next = NULL;
  return true;
}

4.双链表的插入

//在p节点后面插入s节点
bool InsertNextDNode(DNode *p, DNode *s){
  if(p == NULL || s == NULL){
    return false;
  }
  s->next = p->next;
  if(p->next != NULL){
    p->next->prior = s;
  }
  s->prior = p;
  p->next = s;
  return true;
}

5.双链表的删除

//删除p节点的后继节点
bool DeleteNextDNode(DNode *p){
  if(p == NULL){
    return false;
  }
  DNode *q = p->next;
  if(q == NULL){
    return false;       //p无后继节点
  }
  p->next = q->next;
  if(q->next != NULL){
    q->next->prior = p;
  }
  free(q);
  return true;
}

6.销毁双链表

//销毁双链表
void Destory(DLinkList &L){
  //循环释放各个节点
  while(L->next != NULL){
    DeleteNextDNode(L);
  }
  free(L);        //释放头结点
  L = NULL;
}

7.双链表的遍历

void Print(DLinkList p){
  while(p != NULL){
    p = p->next;
    std::cout << p->data << std::endl;
  }
}

5.循环单链表——链表

从一个节点出发,可以找到其他任何一个节点

注意:如果一个链表的应用场景,是需要经常在表头和表尾进行操作,可以将L设置为指向表尾元素,因此在尾插元素时要移动L,时间复杂度为O(1)

1.定义

typedef struct LNode{
  ElemType data;
  struct LNode *next;
}LNode, *LinkList;

2.初始化

bool InitList(LinkList &L){
  L = (LNode*)malloc(sizeof(LNode));
  if(L == NULL){
    return false;
  }
  L->next = L;        //头结点的next指向头结点
  return true;
}

3.判空

bool Empty(LinkList L){
  if(L->next == L){
    return true;
  }
  return false;
}

4.判断是否为尾节点

//判断节点p是否为循环单链表的表尾节点
bool isTail(LinkList L, LNode *p){
  if(p->next == L){
    return true;
  }
  return false;
}

6.循环双链表

1.定义

表头节点的prior指向表尾节点

表尾节点的next指向表头节点

typedef struct DNode{
  ElemType data;
  struct DNode *prior,*next;
}DNode,*DLinkList;

//初始化空的循环双链表
bool InitDLinkList(DLinkList &L){
  L = (DNode*)malloc(sizeof(DNode));
  if(L == NULL){
    return false;
  }
  L->prior = L;
  L->next = L;
  return true;
}

//判空
bool Empty(DLinkList L){
  if(L->next == L)
    return true;
  return false;
}

//判断节点是否为循环链表的表尾节点
bool isTail(DLinkList L, DNode *p){
  if(p->next == L){
    return true;
  }
  return false;
}

2.循环双链表的插入&删除

//在p节点后面插入s节点
bool InsertNextDNode(DNode *p, DNode *s){
  if(p == NULL || s == NULL){
    return false;
  }
  s->next = p->next;
  p->next->prior = s;
  s->prior = p;
  p->next = s;
  return true;
}

//删除p的后继节点
bool DeleteNextDNode(DNode *p){
  if(p == NULL || p->next == NULL){
    return false;
  }
  DNode *q = p->next;
  p->next = q->next;
  q->next->prior = p;    //循环双链表此处不需要判断
  free(q);
  return true;
}

7.静态链表

1.概念

1.单链表,各个节点在内存中随机存储

2.静态链表:分配一整片连续的内存空间,各个节点集中安置

如下图

图片说明

2.定义

#define MaxSize 10
typedef struct {
  ElemType data;
  int next;
}SLinkList[MaxSize];

//SLinkList   a; ===>a是一个数组,每个数组元素是一个struct

3.相关操作

图片说明

图片说明

文件分配表实质上是一个静态链表,因为文件分配表的大小和磁盘的容量相关,容量定了之后,文件分配表的大小也确定;

8.总结

图片说明