Bootstrap

【算法与数据结构】线性表

0. 数据结构

1. 线性表(逻辑结构)

1.1 定义 具有相同数据类型的 n个数据元素 组成的 有限序列。

        除第一个元素,其余元素都有唯一直接前驱;除最后一个元素,其余元素都有唯一直接后继

【注意】线性表是一种逻辑结构,表中元素在逻辑上有顺序性 。顺序表和链表指的是存储结构

1.2 基本操作

InitList(&L)         //初始化表。构造一个空的线性表;
Legnth(L)           //求表长度。返回线性表L的长度,即L中数据元素的个数;
LocateElem(L,e)     //按值查找操作。在表L中查找具有给定关键字值的元素;
GetElem(L,i)        //按位查找操作。获取表L中第i个位置的元素的值;
ListInsert(&L,i,e)   //插入操作。在表L中第i个位置上插入制定元素;
ListDelete(&L,i,e)   //删除操作。删除表L中第i个位置上的元素;
PrintList(L)        //输出操作。按前后顺序输出线性表L的所有元素的值;
Empty(L)            //判空操作。
DestroyList(&L)     //销毁操作。

2. 线性表的顺序表示(顺序表 -- 存储结构)

2.1 定义

        用一组地址连续的存储单元依次存储线性表中的数据元素,故各元素逻辑上相邻,物理上也相邻,支持随机存取。顺序表的存储结构可以描述为:

2.1.1 静态分配:

        弊端:由于数组大小空间已经固定,一旦空间占满,会出现上溢。

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

2.1.2 动态分配:

        注意不是链式存储,依旧是顺序存储!只是在程序执行过程中,一旦数据空间占满,就会另外开辟一个更大的存储空间,去替换原来的存储空间。

#define InitSize 100
typedef struct {
    ElemType *data; //指向数组中的第一元素
    int MazSize, length; // 因为长度可变,所以需要设置一个最大容量
} SqList;

// 动态分配一整片连续的存储空间
L.data = (ElemType *)malloc(sizeof(ElemType) * InitSize);

// 销毁/销毁
free(L.data);

2.1.3顺序存储的特点 

->顺序表最重要的特点是随机访问,即通过首地址和元素序号可以在时间O(1)内找到指定的元素。Loc(a_{i}) = Loc(L) + sizeof(ElemType) * (i - 1)   

->顺序表的存储密度高,每个结点只存储数据元素。(链式存储还需要存放指针)

->顺序表逻辑上相邻,物理上也相邻。删除和插入需要移动大量元素。

->顺序表扩展容量不方便。 

2.2 基本操作

2.2.1 插入操作

最好情况:插入最后一个位子 O(1)

最坏情况:插入第一个位置     O(n)

平均情况:\left [0+1+2+...+n =\frac{ (0+n)(n+1)}{2} \right ] * \frac{1}{n+1} = \frac{n}{2}        所以为: O(n)

// i是位序,从1开始
bool ListInsert( SqList &L, int i, ElemType e ) {
    // i输入不合法
    if ( i < 1 || i > L.length +1 ) {
        return false;
    }
    // 存储空间已满,不能继续插入
    if ( L.length >= MaxSize) {
        return false;
    }
    // i后元素均后移一位
    for ( int j = L.length; j <= i; j-- ) {
        L.data[j] = L.data[j-1];
    }
    L.data[i-1] = e;
    L.length++; 
    return truel;
}

2.2.2 删除操作

最好情况:删除最后一个元素  O(1)

最坏情况:删除第一个位置      O(n)

平均情况:\left [0+1+2+...+(n-1) = \frac{(0+n-1)(n)}{2} \right ] * \frac{1}{n} = \frac{n-1}{2}     所以为O(n)

bool ListDelete( SqList &L, int i, ElemType &e) {
    if( i < 1 || i > L.length + 1 ) {
        return false;
    }
    e = L.data[i - 1]; // 记录删除的元素
    for ( int j = i; j < L.length; j++ ) {
        L.data[j - 1] = L.data[j];
    }
    L.length--;
    return true;
}

2.2.3 按值查找

最好情况:元素在第一个位置        ​​​​​​​          O(1)

最坏情况:元素在不存在(或在表尾)         O(n)

平均情况: \left [1+2+3+...+n = \frac{\left (1+n \right ) * n}{2} \right ] * \frac{1}{n} = \frac{n + 1}{2}     所以为O(n)

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

3. 线性表的链式表示(链表 -- 存储结构)

3.1 单链表

3.1.1 定义

结点描述如下:

typedef struct LNode {
    ElemType data; // 数据域
    struct LNode *next; // 指针域
}LNode, *LinkList;

3.1.2  基本操作

头插法链表的逆置): 每个结点的插入为O(1),设链表长n,则时间复杂度为O(n)

LinkList List_headInsert(LinkList &L) {
    LNode *s;int x;
    L = (LinkList)malloc(sizeof(LNode)); // 创建头结点,成为一个单链表
    L->next = null; // 单链表初始化
    scanf("%d", &x);
    while(x != 9999) {
        s = (LNode *)malloc(sizeof(LNode)); // 分配结点空间
        s->data = x;
        s->next = L->next;
        L->next = s;
        scanf("%d", &x);
    }
    return L; // 返回单链表
}

尾插法:由于设置了一个尾指针p,故每个结点插入为O(1),链表长度为n,时间复杂度为O(n)

LinkList List_tailInsert( LinkList &L ) {
    LNode *s, *p; int x;
    L = (LinkList)malloc(sizeof(LNode));
    L->next = null;
    p = L;
    scanf("%d" , &x);
    while(x != 9999) {
        s = (LNode *)malloc(sizeof(LNode));
        s->data = x;
        s->next = p->next;
        p->next = s;
        p = s;    // p结点往后移
        scanf("%d", &x);
    }
    free(p);    // 释放指针p
    return L;
}

按序查找结点值:时间复杂度为O(n)

LNode *GetElem(LinkList &L, int i) {
    LNode *p = L->next;
    int j = 1;    // 计数,初始为1
    if ( i == 0)  return L;    // i=0时 => 头结点
    if ( i < 1) return null;
    // 这里用for不够完美 无法判别p是否为null
    while ( p && j < i) {
        p = p->next;
        j++;
    }
    return p;
}
        

按值查找表结点:时间复杂度为O(n)

LNode *LocateElem(LinkList *L, ElemType e) {
    LNode *p = L->next;    
    while ( p && p->data != e)  p = p->next;
    return p;
}

插入结点操作:将s结点插入到第i个位置上

该算法主要的时间开销为查找第i-1个结点,时间复杂度为O(n)。若给定p结点,在p结点后插入结点,时间复杂度为O(1)。

p = GetElem(L, i-1); // 找到第i个结点的前驱结点
s-next = p->next;
p->next = s;

若给定p结点,在p结点前插入结点s:我们可以先后插,再交换数据值。

s->next = p->next;
p->next = s;
temp = p->data;
p->data = s->data;
s->data = temp;

删除结点操作:将第i个位置的结点删除。和插入操作类似,时间开销在查找前驱结点上,时间复杂度为O(n),若给定p结点,删除p结点的后一结点,时间复杂度为O(1)。

p = GetElem(L, i-1);
q = p->next;
p->next = q->next;
free(q);

若要删除p结点,可以找到p结点的前驱结点,再删除p结点,但是这样需要花费查找的时间复杂度O(n);所以可以采取另一种算法:删除p的直接后继结点,把后继结点的数据赋给p,这样时间复杂度为O(1)。

q = p->next;
p->data = q->data;
p->next = q->next;
free(q);

求表长操作:计算单链表中(不包含头结点)的结点个数,时间复杂度为O(n)。

3.2 双链表

        由于在单链表中,访问后继结点的时间复杂度为O(1),而访问前驱结点只能从头遍历,时间复杂度为O(n),故引入双链表。

3.2.1 定义

双链表结点结构的描述:

typedef struct DNode {
    ElemType data;
    struct DNode *prior, *next; // 前驱和后继指针
}DNode, *DlinkList;

3.2.2 基本操作 

按位查找和按值查找与单链表无异,插入和删除操作与单链表有区别。

插入(后插)操作:前插操作可以转化为后插操作

// 在p结点后插入结点s
s->next = p->next;
p->next->prior = s;
p->next = s;
s->prior = p;

第二行代码在p是最后一个结点时会报错,需要做进一步判别!

// 在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;
}

删除操作: 

// 删除p的后继结点q
p->next = q->next;
q->next->prior = p;
free(q);

 第二行代码在q是最后一个结点是会出错,所以需要进一步判别!

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

3.3 循环链表

3.3.1 循环单链表

初始化:L->next = L;

3.3.2 循环双链表

初始化:L->next = L; L->prior = L;

基本操作:其插入和删除与双链表的区别是不需要判别p是否为最后一个结点。

3.4 静态链表 

        借助数组来描述线性表的链式存储结构,这里的指针是结点的相对地址(数组下标),又称游标。和顺序表一样,静态链表也需要预先分配一块连续的内存空间。

        适用于操作系统的文件分配表FAT。

结构类型描述如下:

#define MaxSize 50
typedef struct {
    ElemType data;
    int next;
} SLinkList[MaxSize];
[0]                                               头        2
[1]                                                b        6
[2]                                                a        1
[3]                                                d        -1
[4]
[5]
[6]                                                c        3

【注意】 0号结点充当头结点,游标-1表示已经达到表尾

->初始化静态链表:a[0]的next设为-1;

->查找:从头结点出发挨个往后遍历结点,时间复杂度为O(n);

->插入位序为i的结点:

        a. 找一个空的结点存放元素

        b. 找位序为i-1的结点

        c. 修改新结点的next

        d. 修改i-1号结点的next

->删除某个结点:

        a. 找前驱

        b. 修改前驱结点的游标

        c. 被删除结点的next改为-2,表示空闲

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

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

4. 顺序表和链表的比较 

顺序表链表
存取方式随机存取顺序存取
逻辑结构与物理结构逻辑相邻,物理也相邻逻辑相邻,物理不一定相邻
查找、插入和删除操作

顺序表无序:        

        按值查找:O(n)

顺序表有序(折半查找):

        按值查找:O(log{_{2}}^{a}

按位查找:O(1)

插入、删除:O(n),用于移动元素

存储密度大

按值查找:O(n)

按位查找:O(n)

插入、删除:O(n),用于查找元素,但相较于顺序表,链表的时间开销更低

存储密度小

空间分配大片连续空间分配不方便,改善容量不方便离散的小空间分配方便,改变容量方便

 之后有新的知识点会继续补充!

;