数据结构复习——第二章 线性表

本文详细介绍了线性表的定义、特点以及基本操作,包括顺序表和链表两种存储方式。顺序表通过数组实现,支持随机访问,而链表则通过节点连接实现,插入和删除操作更为灵活。此外,文章还提到了单链表、双链表、循环链表和静态链表等变体,以及它们各自的优势和应用场景。
摘要由CSDN通过智能技术生成

一、线性表的定义和基本操作

1.线性表的定义

本节所讲的线性表是一种逻辑结构,表示元素之间一对一的相邻关系。具体实现为顺序表和链表,是指存储结构。

线性表的特点:

  • 表中元素的个数有限
  • 表中元素具有逻辑上的顺序性,表中元素有其先后次序
  • 表中元素都是数据元素,每个元素都是单个元素
  • 表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间
  • 表中元素具有抽象性,即仅讨论元素间的逻辑关系

2.线性表的基本操作

对于大部分数据结构来说,基本操作就是增、删、改、查、创建、销毁。

L:线性表  e:值  i:位序

  • InitList(&L):初始化线性表。即构造一个空的线性表,不放内容
  • Length(L):求表长。传入一个线性表L,返回L的长度,即L中元素个数
  • LocateElem(L,e):按值查找操作。在表L中查找是否存在值为e的元素。在顺序表中,可以获取值为e的元素在线性表中的位置。具体实现按照返回值类型确定。
  • GetElem(L,i):按位查找操作。获取L中第i个位置上的元素值。
  • ListInsert(&L,i,e):插入操作。向表L中的第i个位置插入值为e的元素,并返回一个新的线性表(操作后的)L。
  • ListDelete(&L,i,&e):删除操作。删除表L第i个位置上的元素,并用e返回删除的元素值,返回新表L。
  • PrintList(L):输出操作。按顺序输出顺序表的所有元素值。
  • isEmpty(L):判空操作。判断线性表是否为空表,若空返回true,否则返回false。
  • DestroyList(&L):销毁操作。销毁线性表L,并释放L所占用的内存空间。

【注】:

1.此处使用到的“&”符号表示c++中的引用,目的是可以将修改过后的变量返回。使用指针效果相同

2.只有动态分配的变量和对象才可以使用new和delete

二、线性表的顺序表示

1.顺序表的定义

线性表的顺序存储方式称为顺序表。

顺序表中逻辑顺序与物理顺序相同,它使用了一组地址连续的存储单元,因而存储位置可以用一个简单的公式表示,可以实现随机存取

顺序表特点:需要连续的存储空间,可以随机访问;存储密度高,每个节点只存储元素;插入和删除操作需要移动大量元素;拓展容量不方便

位序:位置顺序=下标+1

顺序表可以有静态分配和动态分配两种方式。静态分配主要就是数组,动态分配是当数据空间占满时再开辟另一块空间。

顺序表实现查找操作(按位)较为简单

2.基本操作的实现

【注】:所有方法的实现最开始都需要有是否合法/存在等等系列条件的判定,之后再实现操作。

顺序表定义:

#define InitSize 10

typedef int Datatype;

//顺序表结构声明
typedef struct LinearList {
    Datatype* data; //数据元素数组指针
    int MaxSize; //最多存放元素个数
    int Length; //表长
}LinearList;

初始化操作:

//初始化顺序表
void InitList(LinearList& list) {
    //list.data = (int*)malloc(sizeof(int) * InitSize); c语言表示法
    list.data = new Datatype[InitSize]; //c++表示法
    list.MaxSize = InitSize;
    list.Length = 0;
}

返回表长:

int Length(LinearList list) {
    return list.Length;
}

按值查找元素:O(n)

//按值查找元素,找到则返回下标,否则返回-1
int LocateElem(LinearList list, Datatype e) {
    for (int i = 0; i < list.Length; i++) {
        if (list.data[i] == e) {
            return i;
        }
    }
    return -1;
}

按位查找元素:O(1)

//按位查找元素,找到则返回值,否则返回-1
Datatype GetElem(LinearList list, int i) {
    if (i<0 || i>list.Length) {
        return -1;
    }
    return list.data[i];
}

插入操作:O(n)

void ListInsert(LinearList& list, int i, Datatype e) {
    //i值不合法
    if (i < 0 || i > list.Length) {
        return;
    }
    for (int j = list.Length; j > i; j--) { //j停止循环时,下标在i的后一位
        list.data[j] = list.data[j - 1]; //令后面的值等于前面的值
    }
    list.data[i] = e;
    list.Length++; //长度+1
}

删除操作:O(n)

//删除操作
void ListDelete(LinearList& list, int i, Datatype& e) {
    if (i < 0 || i >= list.Length) {
        return;
    }
    for (int j = i; j < list.Length - 1; j++) {
        list.data[j] = list.data[j + 1];
    }
    e = list.data[i];
    list.Length--; //长度-1
}

遍历输出:O(n)

void PrintList(LinearList list) {
    for (int i = 0; i < list.Length; i++) {
        cout << list.data[i] << endl;
    }
}

判空操作:

bool isEmpty(LinearList list) {
    return (list.Length > 0) ? false : true;
}

销毁操作:

void DestroyList(LinearList& list) {
    //只有动态分配的部分需要delete(new出来的部分)
    delete list.data;
}

【注】:delete可以销毁的内存只有动态分配给出的部分,其余不需要使用delete清除

三、线性表的链式表示

1.单链表的定义

线性表的链式存储又称为单链表。

每个节点除了存放元素自身信息外,还存放一个指向后继的指针。

单链表的逻辑顺序与物理顺序不同,他通过“链”的方式建立起数据元素之间的逻辑关系。

单链表通过一组任意的存储单元来存储线性表中的数据元素,因而元素在存储空间上的分布是离散的,单链表是非随机存取的存储结构,不能直接找到表中某个特定节点。

单链表实现插入删除操作较为简单

2.基本操作的实现

单链表通常有两种表示形式:“头指针”和“头指针+头结点”。一般使用后者。

使用头结点的优点在于将非空表和空表的处理实现了统一。

(以下所有实现均采用带头结点的方式)

单链表定义:

typedef int Datatype;
//单链表结构声明
//由于单链表实际上是由一个个节点链接而成的,因而只需要写出各个节点的结构即可
typedef struct LinkNode {
    Datatype data; //数据域
    struct LinkNode* next; //指针域(结构体指针)
}LinkNode;
//注:结构体指针才可以使用new关键字

初始化单链表:

//初始化单链表——创建头结点
LinkNode* InitList() {
    LinkNode* head = new LinkNode; //创建一个新节点
    head->next = NULL;
    return head; //返回头节点
}

头插法建表:O(n)

//头插法建表
void HeadInsert(LinkNode* head, Datatype value) {
    LinkNode* node = new LinkNode;
    node->data = value;
    node->next = head->next;
    head->next = node;
}

尾插法建表:O(n)

//尾插法建表
void TailInsert(LinkNode* tail, Datatype value) { //tail为尾指针
    LinkNode* node = new LinkNode;
    node->data = value;
    tail->next = node;
    node->next = NULL;
    tail = node;
}

求表长:

//求表长
int Length(LinkNode* head) {
    LinkNode* p = head->next; //辅助指针p,
    int length = 0;
    while (p != NULL) {
        length++;
        p = p->next;
    }
    return length;
}

【注】:单链表长度不包括头结点

按值查找:O(n)

//按值查找
LinkNode* LocateElem(LinkNode* head, Datatype e) {
    LinkNode* p = head->next;
    while (p != NULL) {
        if (p->data == e) {
            return p;
        }
        p = p->next;
    }
    return NULL;
}

按序号查找:O(n)

//按序号查找
LinkNode* GetElem(LinkNode* head, int i) {
    if (i < 0) {
        return NULL;
    }
    if (i == 0) {
        return head; //序号为0的节点为头结点
    }

    LinkNode* p = head;
    int j = 0;
    while (p != NULL && j != i) {
        j++;
        p = p->next;
    }
    return p;
}

插入操作:O(n)——未给定节点/O(1)——给定节点

//插入操作 插入操作要找到插入位置的前驱节点,也就是第i-1个节点
bool Listinsert(LinkNode* head, int i, Datatype e) {
    LinkNode* pre = GetElem(head, i - 1); //获取插入位置的前驱节点
    if (pre == NULL) {
        return false;
    }

    LinkNode* node = new LinkNode;
    node->data = e;
    node->next = pre->next;
    pre->next = node;
    return true;
}

【扩展】:以上代码进行的是后插操作,需要找到插入位置的前驱节点。若要进行前插操作,则可以将其转换为后插操作实现。还是一样的逻辑,找到前驱节点进行插入,再将两个节点的值互换即可。

删除操作:O(n)——未给定节点/O(1)——给定节点

//删除操作
bool ListDelete(LinkNode* head, int i) {
    LinkNode* pre = GetElem(head, i - 1); //获取删除位置的前驱节点
    if (pre == NULL || pre->next == NULL) {
        return false;
    }
    LinkNode* q = pre->next;
    pre->next = q->next;
    delete(q);
    return true;
}

【扩展】:再删除节点时,我们通常找到其前驱节点进行删除操作。而这需要遍历整个单链表,时间复杂度为O(n)。我们可以将其转换为删除下一个节点,只需要将后继节点的值赋予自身,再删除后继节点即可。这样的时间复杂度为O(1)。

遍历输出:

//输出操作
void PrintList(LinkNode* head) {
    LinkNode* p = head->next;
    while (p != NULL) {
        cout << p->data << endl;
        p = p->next;
    }
}

判空操作:

//判空操作
bool isEmpty(LinkNode* head) {
    return !head->next;
}

销毁操作:

//销毁操作
void DestroyList(LinkNode* head) {
    LinkNode* p = head;
    while (p != NULL) {
        head = p;
        delete(head);
        p = p->next;
    }
    delete p;
}

【注】:

1.顺序表包含了许多属性,因此需要写出顺序表一整个结构体;而链表是由一个个节点构成的,因此只需要写出节点结构体即可

2.只要有遍历,一般都需要创建一个辅助指针

3.在这些操作中,如果向某个方法传入了头结点head指针并对其进行了修改,那么就和&引用类型一样,已经在内存中进行修改,无需返回指针变量,所以返回的值可以设置成其他需要的类型

使用&和*效果相同  &针对一整个结构体,*针对某一个指针/节点
要是创建了结构体变量,要对结构体中的各属性初始化,就使用&引用
要是创建了结构体指针变量,并对指针中的各属性初始化,使用*即可

3.双链表

对于单链表,插入删除操作若不给定节点则只能从头结点依次顺序地向后遍历,且不能够快速的对前驱节点进行操作。

【注】:这里对前驱节点操作不方便,可以转换成对后继节点进行操作,或者使用双链表

双链表克服了单链表的以上问题,每个节点除了data数据域和next指针域,还添加了指向前驱节点的prior指针域。它的优点在于可以方便的找到前驱节点

双链表的查找操作与单链表逻辑相同,但插入和删除操作有不同。

插入操作:(需要改变四个指针)

//插入操作
void DListInsert(DLinkNode* head,int i,Datatype e) {
    DLinkNode* pre = DGetElem(head, i - 1);
    if (pre == NULL) {
        return;
    }

    DLinkNode* node = new DLinkNode;
    node->data = e;

    node->next = pre->next;
    node->next->prior = node;

    node->prior = pre;
    pre->next = node;
}

删除操作:(需要改变两个指针)

//删除操作
void DDeleteList(DLinkNode* head, int i) {
    DLinkNode* pre = DGetElem(head, i - 1);
    if (pre == NULL || pre->next == NULL) {
        return;
    }

    DLinkNode* q = pre->next;
    pre->next = q->next;
    if (q->next != NULL) {
        q->next->prior = pre;
    }
    delete(q);
}

4.循环链表

循环单链表在单链表的基础上,将最后一个节点的next指针指向头结点。因此循环单链表中没有指针域为空的节点。

循环单链表的判空条件也与单链表有所不同。若循环单链表的头结点next指针等于自身,则为空。

循环单链表的优点在于它可以从任意一个节点开始遍历整个链表。

循环双链表就是双链表+循环链表。在循环双链表中,表尾节点的next指针域要指向头结点,头结点的prior指针域要指向尾结点。

因此循环双链表的判空条件就是头结点的next和prior是不是都等于头结点。

5.静态链表

静态链表借助数组来描述线性表的链式结构,同样需要预先分配一块连续的存储空间。他的数据域与链表相同,指针域改为游标(即下一个元素的数组下标)。当next==-1时,链表结束。

静态链表主要应用于不支持指针的高级语言中。

6.*顺序表和链表的比较

①比较

存取方式:顺序表支持顺序、随机存取;链表只能从头遍历,顺序存取

逻辑结构与物理结构:二者均能反映数据间的逻辑关系,只是顺序表的逻辑结构与物理结构相同,链表不同。

查找、插入和删除:

按值查找:顺序表若有序,则可以使用查找算法提高效率。否则两者时间复杂度均为O(n)。

按位查找:顺序表O(1),链表O(n)。

插入删除:顺序表O(n),链表O(1)。

②选择

基于存储:若数据数量变动不大基本固定,则可以采用顺序表,顺序表存储密度大。数量变动大的采用链表,链表存储密度小,且含有指针域,更占内存。

基于运算:若需要频繁的进行插入和删除操作,选用链表。若经常需要按照位序查找元素,采用顺序表。

---------------持续更新中---------------

(欢迎大佬批评指正!)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值