2.1线性表的定义和基本操作
2.1.1线性表的定义 Linear List
线性表是具有相同数据类型的n(n>=0)个元素的有限序列,其中n为表长,当n=0时,线性表是一个空表。若用L命名线性表,则其一般表示为
L=(a1,a2,a3......an)。
ai是线性表中第“i"个元素在线性表中的次序。a1是表头元素,an是表尾元素。
位序从1开始,数组下标从0开始。
除第一个元素之外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。
由于数据类型相同,所以数据元素所占空间一样大,我们可以方便找到每一个数据元素位置。
2.1.2线性表的基本操作
InitList(&L)初始化表。构造一个空的线性表并分配内存空间
DestroyList(&L)销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
ListInsert(&L,i,e)插入操作。在表中第i个位置插入指定元素e
ListDelete(&L,i,e)删除操作。删除表中第i个位置的元素,并用e返回删除元素的值。
LocateElem(&L,e)按值查找。在表中查找具有给定关键字元素的值。
GetElem(&L,i)按位查找。获取表中第i个位置元素的值。
Length(L)求表长。返回线性表L的长度,即数据元素的个数。
Print(L)输出操作。按前后顺序输出线性表L所有元素值
Empty(L)判空操作。若L为空表,则返回true,否则返回false。
Tips:
对数据结构的操作——创销、增删改查。
这里的函数接口,参数类型是抽象的,我们并不关心具体类型。
注意什么时候需要引用传参(对参数的修改结果需要带回来)
2.2线性表的顺序表示
2.2.1顺序表的定义
顺序表:用顺序存储的方式实现线性表
把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
如何知道一个数据元素的大小?sizeof(ElemType)
顺序表的实现——静态分配
#define MaxSize 10//定义最大长度
typedef struct{
ElemType data[MaxSize]//用静态的数组存放数据元素
int length//顺序表的当前长度
}SqList;//顺序表的类型定义(静态分配方式)
void InitList(SqList&L){
for(int i=0;i<MaxSize;i++){
L.data[i]=0;
}
L.length=0;//这一步不能省略,因为内存中可能有之前遗留下来的脏数据
}
如果数组存满了怎么办?
立即推,放弃治疗,顺序表表长确定了就无法更改。
顺序表的实现:动态分配
key:动态申请和释放内存空间。
C语言——malloc free函数 头文件#include<stdlib.h>
L.data=(ElemType)malloc(sizeof(ElemType)*InitSize);
malloc函数返回一个指针,需要强制转化类型。malloc函数参数:指明要分配多大的连续内存空间。
#include<stdlib.h>
using namespace std;
#define InItSize 10//顺序表初始的默认长度
typedef struct {
int* data;//指向动态分配数组的指针
int length;//当前长度
int MaxSize;//最大容量
} SeqList;
void InitList(SeqList& L) {
//用malloc函数申请一片连续的内存空间
L.data = (int*)malloc(InItSize * sizeof(int));
L.length = 0;
L.MaxSize = InItSize;
}
//增加动态数组的长度
void IncreaseSize(SeqList& L,int len) {
int* p = L.data;
L.data = (int*)malloc(sizeof(int) * (InItSize + len));
for (int i = 0; i < L.length; i++) {
L.data[i] = p[i];//将数据复制到新区域
}
L.MaxSize = InItSize + len;//顺序表最大长度增加
free(p);//释放原来的内存空间
}
int main() {
SeqList L;//声明顺序表
InitList(L);//初始化顺序表
IncreaseSize(L, 5);
return 0;
}
C++:new delete关键字。
顺序表特点:随机访问,O(1)时间找到第i个元素。存储密度高,拓展容量不方便,即便是动态数组,也需要很大时间复制元素。删除增加元素更加不方便。
2.2.2顺序表上基本操作的实现
1.插入
ListInsert(&L,i,e)插入操作,在表L中第i个位置插入指定元素.
此刻的i指的是位序,顺序表的位序是从1开始的,但是数组下标是从0开始。
bool ListInsert(SqList& L, int i, int num) {
if (i<1 || i>=L.length + 1) {//插入的i不合法
return false;
}
if (L.length>=MaxSize) {//当前位置已满无法存储
return false;
}
for (int j = length, j >= i, j--) {//i之后所有元素后移一位
L.data[j] = L.data[j - 1];
}
L.data[i - 1] = num;//插入自己想要的元素
L.length++;
return ture;
}
线性表插入操作时间复杂度为O(n)
2.删除操作
bool ListRemove(SqList& L, int i, int &num) {
if (i < 1 || i >= L.length + 1) {//i不合法
return false;
}
num=L.data[i - 1] ;
for (int j = i, j <length, j++) {//i之后所有元素向前移一位
L.data[j-1] = L.data[j ];
}
L.length--;
return ture;
}
注意此次变量num是引用传参,修改带回了。
同时,我们进行删除操作时,元素从前面的开始移,进行插入操作时,元素从最后面的开始移动
2.2.3顺序表的查找
1.按值查找(顺序查找)
LocateElem(L,i)在表中找到具有给定关键字的元素。
int LocateElem(SqList L, int e) {
for (int i = 0; i < length; i++) {
if (L.data[i] == e) {
return i+1;//我们返回的是位序
}
}
return 0;
}
顺序查找时间复杂度为O(n).如果需要判断两个结构类型是否相等,C语言必须分别看结构每个元素是否对应,C++可以重载==运算符(考试时用==运算符是可以的,除非明确说明C语言程序设计)
2.按位查找
GetElem(L,i)获取顺序表L第i个位置的元素的值。
int GetElem(SqList L, int i) {
if (i < 1 || i >= L.length + 1) {//i不合法
return 0;
}
return L.data[i - 1];//即便是动态分配内存也可用此方法
}
顺序表特性就是随机存取,时间复杂度O(1)
2.3 线性表的链式表示
2.3.1 单链表的定义
每个结点存储自身的数据,还存放指向下一个节点的指针。
王道书上所说的头节点,实际上是我学过的虚拟头节点方式,最好还是用dummy_head ,便于我们统一操作。
typedef struct LNode{
ElemType data;
struct LNode*next;
}LNode,*LinkList;
LNode*与 LinkList本质上是一样的,只不过LNode*强调这是一个结点类型,LinkList强调引入的是一个链表。
创建不带虚拟头结点的链表,判断链表是否为空:L==NULL;
带虚拟头结点的链表判断为空:L->next==NULL;
2.3.2 单链表的插入删除
按位序插入:在第i个位置插入元素e(带虚拟头结点)
bool ListInsert(LinkList& L, int i, int e) {
if (i < 1)return false;
LNode* p;//指针p用来扫描
int j = 0;//记录指针p当前扫描到第几个结点
p = L;//L指向头节点,头节点是第0个节点
while (p != NULL && j < i - 1) {//循环找到第i-1个节点
j++;
p = p->next;
}
if (p == NULL)return false;
LNode*cur=(LNode*)malloc(sizeof(LNode));
cur->data = e;
cur = p->next;
p->next = cur;
return ture;
}
指定结点的后插操作:在p结点后插入元素e
bool InsertNextNode(LNode* p, int e) {
if (p == NULL)return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
if (s == NULL)return false;//极少数情况下内存不够分配失败
s->data = e;
s = p->next;
p->next = s;
return true;
}
指定结点的前插操作:此时会出现问题,单向链表并不知道前面是什么,所以要么传入头指针, 依次遍历,要么“偷天换日”。
bool InsertPriorNode(LNode* p, int e) {
if (p == NULL)
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
if (s == NULL)
return false;
s->next = p->next;
p->next = s;//先把结点插入进去,然后交换数据
s->data = p->data;
p->data = e;
return true;
}
按位序删除(带虚拟头结点)
bool ListInsert(LinkList& L, int i, int &e) {
if (i < 1)return false;
LNode* p;//指针p用来扫描
int j = 0;//记录指针p当前扫描到第几个结点
p = L;//L指向头节点,头节点是第0个节点
while (p != NULL && j < i - 1) {//循环找到第i-1个节点
j++;
p = p->next;
}
if (p == NULL)return false;
LNode *q=p->next;
e=q->data;
p->next=q->next;
free(q);
return ture;
}
指定结点的删除
bool DeleteNode(LNode* p) {//这里的删除只是值改变了,实际的指针没变
if (p == NULL)
return false;
LNode* q = p->next;
p->data = p->next->data;
p->next = q->next;
free(q);
return true;
}
但是,如果p是最后一个结点,那么我们操作p->next->data就会出现问题,此时要么重新遍历查找,要么就这样算了,考试时最多扣一分甚至不扣分。
2.3.3单链表的查找
按位查找
LNode* GetElem(LinkList L, int i) {
int j = 0;
LNode* p;
p = L;
if (i < 1)
return NULL;
if (i == 0) {
return L;
}
while (p!= NULL && j < i) {
j++;
p = p->next;
}
return p;
}
按值查找
LNode* LocateElem(int val, LinkList& L) {
LNode* p = L->next;
while (p != NULL && p->data != val) {
p = p->next;
}
return p;
}
2.3.4 单链表的建立
头插法建立单链表:
void HeadInsert (LNode*& L, int a[], int n) {
L = (LNode*)malloc(sizeof(LNode));
L->next = NULL;//初始化的工作不能忘记!
LNode* s;
for (int i = 0; i < n; i++) {
s = (LNode*)malloc(sizeof(LNode));
s->data = a[i];
s->next = L->next;//头插法的关键步骤
L->next = s;
}
}
尾插法建立单链表:
void TailInsert(LNode*& L, int a[], int n) {
L = (LNode*)malloc(sizeof(LNode));
L->next = NULL;
LNode* s,*r;
r = L;//头结点不能随意移动
for (int i = 0; i < n; i++) {
s = (LNode*)malloc(sizeof(LNode));
s->data = a[i];
//尾插法的关键步骤
r->next = s;
r = s;//r是一个指向终点的指针,每次都要移动
}
r->next = NULL;
}
2.3.5 双链表
双链表的定义:
typedef struct DLNode{
int data;
DLNode* next,*prior;
};
双链表的插入操作:
双链表的插入操作容易出现错误,1、2两行代码必须出现在第四行代码之前,否则会缺失指针。
p结点是原有结点,s结点为要插入的结点。
s->next=p->next;
s->prior=p;
p->next->prior=s;
p->next=s;
双链表的删除操作:
q=p->next;//保留p后继结点的功能。
q->next->prior=p;
p->next=q->next;
free(q);
双链表的建立:尾插法
void TailInsert(DLNode*& L, int a[], int n) {
DLNode* s, * r;
L = (DLNode*)malloc(sizeof(DLNode));
r = L;
L->next = NULL;
L->prior = NULL;
for (int i = 0; i < n; i++) {
s = (DLNode*)malloc(sizeof(DLNode));
s->data = a[i];
r->next = s;
s->prior = r;
r = s;
}
r->next = NULL;
}
2.3.6循环链表
循环单链表:循环单链表中最后的next指针指向L;判空条件:带头结点的循环单链表head=head->next;链表为空,不带头节点的循环单链表,head=NULL时为空。
有时我们只设置尾指针而不设置头指针,这是因为尾指针效率更高(头指针想要遍历到尾,需要O(N)复杂度,但是尾指针只需要tali->next即可到达头指针)。
循环双链表:尾结点的next指向头节点,所有的next形成闭环,所有的prior形成闭环。
判空条件:不带头结点:head=NULL;带头结点:此时双链表是没有空指针的,只需要检查head->next==NULL||head->prior==head;二者只要有一个等于head即可判空。
2.3.7静态链表
在一些不支持指针的语言如basic中会用数组来模拟链表,它实际上利用游标来模拟指针,这里的指针指的是结点的相对地址(又称数组下标)。与顺序表一样,静态链表也需要一开始分配起始的存储空间。
#define MaxSize 20
typedef struct {
int data;
int next;
}StaticLinkList[MaxSize];
2.4顺序表的常见考法
2.4.1 元素逆置
给定一个顺序表,如何将其中元素逆置?可以设置两个整形变量i,j一个指向左端,一个指向右端,不断交换二者对应的值,直到二者相遇。
void reverse(int a[],int left,int right){
for(int i=left,int j=right;i==j;i++,j--){
int temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}