编程语言
首页 > 编程语言> > 数据结构与算法——线性表(完结)

数据结构与算法——线性表(完结)

作者:互联网

线性表

定义和基本操作

定义

线性表是具有相同

数据类型的n(n大于0)个数据元素的有限序列

,其中n为表长,当n=0时线性表是一个空表。若用L命名线性表,则其一般表示为
$$
L = (a1, a2, ..., ai, ai+1, ..., an)
$$
ai时线性表中第i个元素线性表中的位序

a1表头元素

an表尾元素

除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后驱。

基本操作

一个数据结构的基本操作是指最核心、最基本的操作。其它较复杂的操作可通过调用其基本操作来实现。线性表的主要操作如下:

// 1. 初始化表。构造一个空的线性表
InitList(&L)
    
// 2. 求表长,返回线性表L的长度,即L中数据元素的个数
Length(L)
    
// 3. 按值查找操作。在表L中查找具有给定关键字值得元素
LocateElem(L, e)
// 4. 按位查找操作。获取表L中第i个位置的元素的值

Tips:

  1. 对数据的操作——创建销毁、增删改查
  2. c语言函数的定义需要指定参数类型
  3. 实际开发中,可根据实际需求定义其它的基本操作
  4. 函数名和参数的形式、命名都可以改变(Reference:严蔚敏版)
  5. 当需要对参数的修改结果“带回来”的时候传入引用 &

为什么要实现对数据结构的基本操作?

  1. 团队合作编程,你定义的数据结构要让别人可以很方便的使用。
  2. 将常用的操作或运算封装成函数,避免重复工作,降低出错风险。

总结

线性表的物理结构

线性表的顺序表示

线性表的定义

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

假定线性表的元素类型为ElemType,则线性表的顺序存储类型描述为:

#define MAXSIZE 50
typedef struct {
    ElemType data[MAXSIZE];
    int length;
}

此种表示方式是由缺陷的。如果当第51个元素到来时,它是没有地方可以插入的。也就是说,一旦空间占满,在加入新的数据就会产生溢出,进而导致程序崩溃。

所以引出动态分配。

动态分配时,存储空间是在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满,就需要开辟一块更大的存储空间,用以替代原来的存储空间,从而达到扩充数据空间的目的,而不需要为线性表一次性的划分所有空间。

#define INITSIZE 50
typedef struct {
    // 动态分配数组的指针
    ElemType *data;
    // 数组的最大容量
    int maxsize;
    // 数组的当前个数
    int length;
}

C的初始动态分配语句为

L.data = (ElemType *)malloc(sizeof(ElemType) * INITSIZE)

C++的初始化分配语句为

L.data = new ElemType[INITSIZE]

顺序表的最主要特点是随机访问,即通过首地址和元素需要可在时间O(1)内找到指定的元素。

顺序表的存储密度高,每个节点只存储数据元素。

顺序表逻辑相邻的元素物理上也相邻,所以插入和删除操作需要移动大量的元素。

顺序表上基本操作的实现

插入操作
boll ListInsert(SqList &L, int i, ElemType e) {
    if (i < 1 || i > L.length)
        return false;
    if (L.length > MAXSIZE) 
        return false;
    for(int j = L.length; j >= i; j--) {
        L.data[j] = L.data[j-1]
    }
    L.data[i-1] = e;
    L.length++;
    return true;
}
删除操作
bool ListDelete(SqList &L, int i, int &e) {
    if (i < 1 || i > L.length)
        return false;
    e = data[i-1];
    for(int j = i; j < L.length; j++) {
        L.data[j-1] = L.data[j];
    }
    L.length--;
    return true;
}

最好情况:删除表尾元素,时间复杂度为O(1)

最坏情况:删除表头元素,时间复杂度为O(n)

平均情况:i=1循环n-1次;i=2循环n-2次

故为 0 + 1 + 2 + ... + n-1 平均为(n-1) / 2

按值查找
int LocateElem(SqList L, ElemType e) {
    for(int i=0;i<L.length;i++) {
        if(L.data[i] == e)
            return i+1;
    }
    return 0;
}

最好情况:目标元素在表头,循环一次 O(1)

最坏情况:目标元素在表尾,循环n次 O(n)

平均情况:(1 + 2 + ... + n)/n = (n+1)/n

按位查找

静态分配

ElemType GetElem(SqList L, int i) {
    if (i < 1 || i > L.length) {
        return null;
    }
    return L.data[i-1];
}

《数据结构》考研初试中,手写代码可以直接使用“==”,无论是结构体类型还是基本结构类型。

基本操作小结

线性表的链式存储

单链表的定义

线性表的链式存储又称单链表,它是通过一组任意的存储单元来存储线性表中的数据元素。

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

缺点:不可随机存储,要耗费一定空间存放指针。

单链表节点类型的描述如下:

// 定义单链表节点类型
typedef struct LNode {
    // 数据域
    ElemType data;
    // 指针域
    struct LNode *next;
}LNode, *LinkList;

// 增加一个新的节点:在内存中申请一个节点所需空间,并用指针p指向这个节点
struct LNode *p = (struct LNode*) malloc(sizeof(struct LNode));

typedef的作用是对数据类型重命名。

使用LinkList时表示 强调这是一个单链表

使用LNode时表示 强调这是一个节点

不带头节点的单链表初始化
bool InitList(LinkList &L) {
    // 空表,暂时还没有任何节点
    L = NULL;
    return true;
}

int main() {
    // 声明一个指向单链表的指针
    LinkList L;
    // 初始化一个空表
    InitList(L);
}

不带头节点,写代码更麻烦

对第一个数据结点和后续数据结点的处理需要用不同的代码逻辑。对空表和非空表的处理需要用不同的代码逻辑。

带头节点的单链表初始化
bool InitList(LinkList &L) {
    // 分配一个头节点
    L = (LNode *)malloc(sizeof(LNode));
    // 内存不足,分配失败
    if(L == NULL)
        return false;
    // 头节点之后暂时还没有其它的节点
    L->next = NULL;
    return true;
}

int main() {
    // 声明一个指向单链表的指针
    LinkList L;
    // 初始化一个空表
    InitList(L);
}

单链表上基本操作的实现

按位序插入(带头结点)
// 在第i个位置插入元素e
bool ListInsert(LinkList &L, int i, ElemType e) {
    if(i<1)
        return false;
    // 指针p指向当前扫描到的结点
    LNode *p;
    // 当前p指向的是第几个结点
    int j = 0;
    // L指向头结点,头结点是第0个结点,不存数据
    p = L;
    while(p!=NULL && j < i-1) {
        p = p->next;
        j++;
    }
    if (p == NULL)
        return false;
    LNode *s = (LNode*)malloc(sizeof(LNode));
    s->data = e;
    s->next = p->next;
    p->next = s;
    return true;
}
按位序插入(不带头结点)

不带头节点写代码更不方便,推荐使用带头节点。

但在考试中,两种都有可能进行考察,所以需要了解。

bool ListInsert(LinkList &L, int i, ElemType e) {
    if (i < 1)
        return false;
    if (i == 1) {
        LNode *s = (LNode *)malloc(sizeof(LNode));
        s->data = e;
        s->next = L;
        L = s;
        return true;
    }
    // 指针p指向当前扫描到的结点
    LNode *p;
    // 当前p指向的是第几个结点
    int j = 1;
    // L指向第一个结点
    p = L;
    while(p!=NULL && j < i-1) {
        p = p->next;
        j++;
    }
    if (p == NULL)
        return false;
    LNode *s = (LNode*)malloc(sizeof(LNode));
    s->data = e;
    s->next = p->next;
    p->next = s;
    return true;
    
}
指定结点的后插操作
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;
}
指定结点的前插操作
bool InsertPriorNode(LNode *p, ELemType 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 ListDelete(LinkList &L, int i, ElemType &e) {
    if(i<1)
        return false;
    LNode *p;
    // 当前p指向的是第几个结点
    int j = 0;
    // 指向头结点
    p = L;
    while (p != NULL && j < i-1) {
        p = p->next;
        j++;
    }
    if (p == NULL || p->next == NULL)
        return false;
    
    LNode *q = p->next;
    e = q->data;
    p->next = q->next;
    free(q);
    return true;
}
指定结点删除
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;
}

单链表的局限性:无法逆向检索,有时候不太方便。

按位查找
// 按位查找,返回第i个元素(带头结点)
LNode* GetElem(LinkList L, int i) {
    if (i < 0)
        return NULL;
    LNode *p;
    int j = 0;
    p = L;
    while (p != NULL && j < i) {
        p = p->next;
        j++;
    }
    return p;
}
按值查找
LNode* LocateElem(LinkList L,ElemType e) {
    LNode *p = L->next;
    // 从第一个结点开始查找数据域为e的结点
    while(p != NULL && p->data !=e) {
        p = p->next;
    }
    return p;
}
求表的长度
int Length(LinkList L) {
    int len = 0;
    LNode *p = L;
    while(p->next != NULL) {
        p = p->next;
        len++;
    }
    return len;
}

我们看出了单链表无法逆向检索的缺点,即找到p结点的前驱结点,所以引出了双链表。

双链表

双链表结点中有两个指针prior和next,分别指向前驱结点和后继结点。

双链表逻辑结构

双链表中结点类型描述如下:

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

双链表的初始化

// 初始化双链表
bool InitDLinkList(DLinkList &L) {
    L = (DNode*)malloc(sizeof(DNode));
    // 内存不足则分配失败
    if (L == NULL) {
        return false;
    }
    // 头结点的prior指针永远指向null
    L->prior = NULL;
    // 头结点之后暂时还没有其它的结点
    L->next = NULL;
    return true;
}

双链表的插入

// 在p结点之后插入s结点
bool InsertNextDNode(DNode *p, DNode *s) {
    // 判断非法参数
    if (p == NULL || s == NULL) {
        return false;
    }
    s->next = p->next;
    // 如果p没有后继结点
    if (p->next->prior != NULL) {
        p->next->prior = s;
    }
    
    p->next = s;
    s->prior = p;
    return true;
}

双链表的删除

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

双链表的遍历

// 后向遍历
while (p != NULL) {
    // 对结点p做相应处理
    p = p->next;
}

// 前向遍历
while (p != NULL) {
    // 对结点p做相应处理
    p = p->prior;
}

// 前向遍历(跳过头结点)
while (p->prior != NULL) {
    // 对结点p做相应处理
    p = p->prior;
}

双链表不可随机存取,按位查找、按值查找操作都只能用遍历方式实现。时间复杂度为O(n)

双链表总结

循环链表

循环单链表

循环单链表逻辑结构

表尾结点next指回了头结点。

初始化循环单链表
// 初始化循环单链表
bool InitList(LinkList &L) {
    L = (LNode *) malloc(sizeof(LNode));
    if (L == NULL)
        return false;
    L->next = L;
    return true;
}
// 判断循环单链表是否为空
bool Empty(LinkList L) {
    return L->next == L  
}

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

循环双链表

静态链表

定义

借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,与前面所讲的链表中的指针不同的是,这里的指针是结点的相对地址,又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。

优点:增删操作不需要大量移动元素

缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变

适用场景:

  1. 不支持指针的低级语言;
  2. 数据元素数量固定不变的场景(如操作系统的文件分配表FAT)

标签:结点,NULL,return,线性表,next,完结,数据结构,data,LNode
来源: https://www.cnblogs.com/Gazikel/p/16369623.html