数据结构第四篇——(一般)线性表(基于C语言)

Posted 从零开始的智障生活

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构第四篇——(一般)线性表(基于C语言)相关的知识,希望对你有一定的参考价值。

 前言

以下描述内容以C语言为主,Java只是作为实现的补充。

 一、线性表的定义及性质

线性表是由n(n>=O)个数据类型相同的元素构成的有限序列。

线性表中元素的个数n(n>=O)定义为表长,n=O时称为空表。

线性表按照存储结构的不同,又划分为顺序表和链式表。

1.1 顺序表的定义及其特点

顺序表(Sequential List):用一组地址连续的存储单元依次存储线性表的数据元素。

其特点是:

  • 逻辑上相邻的数据元素,其物理次序也是相邻的;
  • 随机访问,可以在O(1)时间内找到第i个元素;
  • 存储密度高,每个结点只存储数据元素;
  • 扩展容量不方便(即使采用动态分配的方式,拓展长度的时间复杂度都很高)。

假设线性表的每个元素需占用T个存储单元,并以占用的第一个单元的存储地址作为数据元素的起始位置。线性表中第i+1个元素的存储位置LOC(ai+1)和第i个数据元素的存储位置LOC(ai)之间的关系是LOC(ai+1)= LOC(ai)+T。线性表的第i个数据元素ai的存储位置为:LOC(ai)=LOC(a1)+(i-1)*T。但指针只需要*(p+1)。

1.2 链式表的定义及其特点

链式表(Linked List):用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续或非连续的)。

特点:

  • 逻辑上相邻的数据元素,物理上不用相邻
  • 顺序访问结构,访问任意元素的平均时间复杂度是O(n)
  • 存储密度低,每个结点包括数据域和指针域。
  • 插入删除的时间复杂度低

二、线性表的逻辑结构,以及基本操作

 用抽象数据类型,来描述线性表的逻辑结构

三、线性表的抽象数据类型的实现

3.1 顺序存储结构线性表基本操作的实现

先拆分开来看,完整代码会在后面给出。

3.1.1 创建与初始化基于顺序存储结构的线性表

顺序表的初始化:分为静态分配和动态分配 参考

静态分配:用静态的“数组”存放数据元素;缺点:顺序表大小容量不可修改,容易浪费内存资源或造成溢出。

补充,用Java实现,其实就是数组,没什么要补充的。

import java.util.Arrays;

/**
 * @author zyx
 *	用静态分配的顺序存储结构创建线性表
 */
public class SLOfStaticAllocated {
	private final static int MaxSize = 10;
	
	private int[] data = new int[MaxSize]; // 创建数组,并分配内存空间。
	private int length = 0; // 数组长度
	
	// 初始化线性表
	SLOfStaticAllocated() {
		Arrays.fill(data, 0);// 全部填充为0
	}
}

动态分配:即用指针方式指向一片连续区域的首地址。

3.1.2 实现基于顺序存储结构的线性表基本功能

扩展表

  1. 用p保存顺序表的内容,
  2. 为顺序表重新申请一片连续的新容量大小的区域
  3. 将保存到数据复制到新的区域。

int IncreaseList(SqList *L,int len){

        int *p = L->data;

        //指针指向一块新的特定大小的区域

        L->data = (int *)malloc((L->MaxSize+len)*sizeof(int));

        if(!L->data){

                 printf("容量增加失败\\n");

                 exit(OVERFLOW);

        }

        int i = 0;

        while(i!=L->length){

                 L->data[i] = p[i];

                 i++;

        }

        L->MaxSize = L->MaxSize+len;

        free(p); //释放原内存块

        return 1;

}

插入

1<=i<=L->lenght+1,在第i个结点前插入节点e,平均时间复杂度为O(n)

边界条件:

  1. Length达到Maxsize

不合法条件:

  1. i<1
  2. i>length+1

注意最后如果插入成功,那么length+1

int ListInsert(SqList *L,int i,int e){

        // 1.判断插入位置i是否合法(i值的合法范围是l<=i<=L->length+1), 若不合法 则返回 ERROR。

        if(i<1||i>L->length+1){

                 printf("i值不合法\\n");

                 return ERROR; //i值不合法

        }

        // 2.判断顺序表的存储空间是否已满,若满见肤返回 ERROR。

        if(L->length == L->MaxSize) {

                 printf("存储空间已满\\n");

                 return ERROR; //存储空间已满

        }

        // 3.将第n个至第i个位置的元素依次向后移动一个位置,特别地空出第l个位置(i=L->length+l 时无需移动)。

        for (int j = L->length; j >=i ; j--)// L->data[L->length] = L->data[L->length-1]

        {

                 L->data[j] = L->data[j-1];

        }

        L->data[i-1] = e;

        L->length++;

        return 1;

}

删除结点

删除第i个元素,平均时间复杂度是O(n)

int ListDelete(SqList *L,int i){

        // 1.判断删除位置i(1<=i<=L->length)是否合法

        if(i<1||i>L->length) return ERROR;

        // 2. 将第i+1个元素至第L->length个元素,依次向前移动一个位置(第L->length个元素时无需移动)。

        for (int j = i; j < L->length; ++j)

        {

                 L->data[j-1] = L->data[j];

        }

        // 3.表的长度减1

        L->length--;

        return 1;

}

根据位置取值

int GetElem(SqList L,int i,int *e){

        // 1.判断查找位置i(1<=i<=L.length)是否合法

        if(i<1||i>L.length) return ERROR;

        *e = L.data[i-1];

        return 1;

}

根据关键值索引

int LocateElem(SqList L,int e){

        for (int i = 0; i < L.length; ++i)

        {

                 if(e==L.data[i]) return i+1;

        }

        return 0; // 查找失败,未找到

}

列表逆序

void ReverseList(SqList *L)

{

        if (L->length)

                 for (int i = 0; i<L->length - 1 - i; i++)

                 {

                         int t = L->data[i];

                         L->data[i] = L->data[L->length - 1 - i];

                         L->data[L->length - 1 - i] = t;

                 }

}

列表长度

int Length(SqList L){

        return L.length;

}

列表容量

int ListSize(SqList L){

        return L.MaxSize;

}

列表是否为空

boolean Empty(SqList L){

        return L.length==0;

}

销毁列表

顺序表因为是数据域data就代表整个区域,所以只要free(L)

即可。

int DestroyList(SqList *L){

        free(L);

        if(!L->data)

                 return 1;

        else

                 return ERROR;

}

遍历列表

int TravelList(SqList L){

        for (int i = 0; i < L.length; ++i)

        {

                 printf("%d\\n",L.data[i]);

        }

        return 1;

}

 用顺序存储结构实现线性表其实比较少,对于一些比较复杂的线性表结构,更多的是用链式存储结构实现。

3.2 用链式存储结构的实现技术,比如单向链表、双向链表、单循环链表、双向循环链表以及带头节点的链表,以及其对线性表基本操作的实现

3.2.1 单链表

要表示一个单链表时只需声明一个头指针,指向单链表的第一个结点。

定义一个单链表结点类型

typedef struct LNode // 定义单链表结点类型

{

        int data;   // 每个结点存放一个数据元素

        struct LNode *next; // 指针指向下一节点

}LNode,*LinkList;

LNode *L; // 声明一个指向单链表第一个结点的指针

LinkList L; // 声明一个指向单链表第一个结点的指针

// 这两个效果等价,但这种命名方式增加可读性

// LNode 强调结点,而LinkList强调单链表,

// 返回值是LNode  *强调返回单链表的一个结点

// 返回值是LinkList 强调返回一个单链表

初始化:

初始化一个单链表(无头结点)

bool InitList(LinkList L){ // 注意L是指针类型

        L = NULL; // 空表,暂时无任何数据

        return true;

}

头指针L->NULL

头指针指向的下一节点,如果存在的话就是具有有效数据的结点。

空表判断L==NULL

初始化:

初始化一个单链表(有头结点)

bool InitList(LinkList L){

        // 生成新节点作为头结点,用头指针L指向头结点

        L = (LNode *)malloc(sizeof(LNode));

        if(L==NULL) return false;

        L->next = NULL; // 头结点的指针域置空

        return true;

}

头指针指向头结点,但头结点的数据没有任何实际价值,头结点的下一节点才是具有有效数据的结点。

空表判断L->next==NULL

插入节点:

按位序前插(带头结点):

将值为e的新节点插入到表第i个结点的位置上,即插入到第ai个结点前面。

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个元素且当前节点不为空

                 p = p->next;

                 j++

        }

        if(p==NULL) return false; // i值不合法

        LNode *s = (LNode *)malloc(sizeof(LNode)); // 为新节点申请空间

        s->data = e;

        s->next = p->next;

        p->next = s;

        return true;

}

从上面带头结点的初始化可以看出,传入的参数LinkList L是代表链表,也代表头结点。

提示:这里结点的数据是存放在某一地址中,地址只能由指针指向,所以结点的数据类型只能是结点指针。

而结点指针指向的只是一个结点空间的地址,而这个地址存放该节点的数据和下一个结点的地址。

为什么用指针,而不直接用一个结构体变量定义结点?

如果用非指针变量a定义结点,那么这就不是表了,而是一个个独立的数据元素,而且,指针p,p->next = &a;或p->next =a效果一样,但是这样一个100 项的列表要定义100个变量,很不方便。

插入节点:

指定结点的后插

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->next = p->next;

        p->next = s;

        return true;

}

不用考虑是否带头结点

插入节点:

指定节点的前插操作

bool InsertPriorNode(LNode *p,LNode *s){

        if(p==NULL||s==NULL) return false;// 1.验证有效

        s->next = p->next; // 2. 先进行后插

        p->next = s;

        int temp = p->data; // 3. 将p结点数据与s结点数据交换

        p->data = s->data;

        s->data = temp;

        return true

}

因为不知道前驱结点,而且两个结点反正是在同一链表上,交换结点,就等价于交换结点数据,所以只要后插,然后交换结点数据就可以了。

不用考虑是否带头结点

删除操作:

按位序删除删除

bool ListDelete(LinkList L,int i,int *e){

        if (i<1) return false;

        LNode *p; // 指针p扫描到当前节点

        int j = 0; // j表示到第几个结点

        P = L;

        // 找到要删除结点的前驱结点

        while(p!=NULL && j<i-1){

                 p = p->next;

                 j++;

        }

        if(p==NULL) return false;

        // 将前驱结点与后继结点连接

        LNode *q = p->next; //令q指向被删除结点

        *e = q->data; // e返回删除的值

        p->next = q->next; // 将前驱结点与后继结点相连

        free(q); // 释放删除结点内存

        return true;

}

这里LinkList L和LNode *p=L的实际意义是一样的,但是这样做的意义是:L代表当前链表,p代表节点指针,用来指向当前节点,增加可读性。

删除结点一定要保证:链表不断;

链表不断一定要保证:前驱结点与后继结点相连。

删除结点:

指定节点删除

bool DeleteNode(LNode *p){

        if(p==NULL) return false;

        LNode *q = p->next; // 用q指向后继结点

        if(q==NULL){//如果p是尾结点,那么p->next==NULL

                 p = NULL;

                 return true;

        }

        p->data = p->next->data; // 和后继节点交换数据

        p->next = q->next; // 将后继结点从链表中剔除

        free(q); // 释放后继结点的存储空间;

        return true;

}

难点在于,只有一个要删除的结点,没有前驱结点,删除当前节点,后面的就没了。

思想:将删除结点等价为删除结点数据。

注意:为了保证链表不断,需要交换结点,只要将后继结点的数据放到前面,不用交换,因为要删除,里面是什么都不用考虑。

优点在于时间复杂度为O(1)

查找:

按位查找,返回第i个元素(带头结点)

LNode *GetElem(LinkList L,int i){

        if(i<0) return NULL;

        LNode *p;

        j = 0;

        p = L;

        while(p!=NULL && j<i){

                 p = p->next;

                 j++;

        }

        return p;

}

注意:考虑边界情况,增加代码健壮性,如i<0或i超过链表长度。

平均时间复杂度:O(n)

查找:

按值查找,找到数据域==e的结点

LNode *LocateElem(LinkList L,int e){

        LNode *p = L;

        while(p!=NULL && p->data != e){

                 p = p->next;

        }

        return p;

}

  1. 能找到的情况
  2. 找不到的情况

求表长

int Length(LinkList L){

        int len = 0;

        LNode *p = L;

        while(p->next!=NULL){

                 p = p->next

                 len++;

        }

        return len;

}

建表:

尾插法

LinkList ListTailInsert(LinkList L){

        int x;// 新节点的数据

        L = (LinkList)malloc(sizeof(LNode));//建立头结点 初始化空表

        // LNode *s,*t = L;//s表示新的结点,t表示尾结点

        LNode *s;

        LNode *t = L;

        scanf("%d",&x);

        while(x!=-1){

                 s = (LNode *)malloc(sizeof(LNode));// 为新节点分配空间

                 t->next = s;

                 s->data = x;

                 t = s;

                 scanf("%d",&x);

        }

        t->next = NULL; //尾结点置空

        return L;

}

尾插法建立单链表,是正向建立单链表,比起每次尾插一个结点,循环一次,时间复杂度为O(n^2),不如定义一个尾结点,用来标记尾巴。

这里面是将链表初始化的过程包含了。时间复杂度是O(n)

建表:

头插法

LinkList ListHeadInsert(LinkList L){

        int x;

        LNode *s;

        L = (LinkList)malloc(sizeof(LNode));

        L->next = NULL;

        scanf("%d",&x);

        while(x!=-1){

                 s = (LNode *)malloc(sizeof(LNode));//为新节点分配空间

                 s->data = x;

                 s->next = L->next;

                 L->next = s;

                 scanf("%d",&x);

        }

        return L;

}

也就是每次对头结点进行后插操作。

注意:养成好习惯,只要是初始化单链表,都先把头指针指向NULL,不一定有影响,但是可以防止在没有初始化的情况下发生意外。

逆序单链表(带头结点)

LinkList ListReverse(LinkList L){

        // 1. 空链表,或者只有一个结点的链表

        if(L==NULL||L->next==NULL) return NULL;

        // 2. 按顺序将所有剩下的结点对【头结点的后一个结点L->next】进行头插

        // 直到剩余结点为空

        LNode *s = L->next;// 当前头结点的下一节点,之后的标志,不变

        LNode *r = s->next; // 当前待头插的结点,一直成为L->next

        while(s->next!=NULL){

                 s->next = r->next; // H->A->B->C->D变成H->A->C->D,B

                 r->next = L->next; // 再变成H->B->A->C->D

                 L->next = r;

                 // 下次循环H-B-A-C-D变成H-B-A-D,C

                 // 再变成H-C-B-A-D

        }

        return L;

}

逆序头结点不作考虑,位置不变。如果L==NULL,那么只有空链表,如果L->next==NULL,那么就表示只有一个结点,那就不用逆序。

我们将L->next->next结点B,对结点L->next进行头插。

而s=L->next之后将作为标志来获取下一个待头插结点。

3.2.2 双链表

声明双链表类型

typedef struct

{

        int data;

        struct DNode *prior,*next;

}DNode,*DLinkList;

DNode *等价于DlinkList;

初始化双链表

bool InitDLinkList(DLinkList L){

        L = (DLinkList)malloc(sizeof(DNode));

        if(L==NULL) return false;

        L->prior = NULL;

        L->next = NULL;

        return false;

}

判断双链表是否为空:

L->next == NULL

插入:

在结点*p后插入节点*s。

bool InsertNextDNode(DNode *p,DNode *s){

        if(p==NULL||s==NULL) return false;

        s->next = p->next;// 将*s结点插入到结点*p之后。

        p->next->prior = s;

        s->prior = p;

        p->next = s;

        return true;

}

删除:

删除p结点的后继结点

删除p结点的后继结点

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;

}

销毁链表

bool DestroyDList(DLinkList L){

        while(L->next!=NULL){

                 DeleteNextDNode(DLinkList L);

        }

        free(L);//释放头结点

        L=NULL;头指针指向NULL

        return true;

}

双链表的遍历:

后向遍历

void BackTraveler(DNode *p){

        while(p!=NULL){

                 printf("%d\\n",p->data);

                 p=p->next;

        }

}

双链表的遍历:

前向遍历(包括头结点)

void ForeTraveler(DNode *p){

        while(p!=NULL){

                 printf("%d\\n",p->data);

                 p = p->prior;

        }

}

双链表的遍历:

前向遍历(不包括头结点)

void ForeTraveler2(DNode *p){

        while(p->prior!=NULL){

                 printf("%d\\n",p->data);

                 p = p->prior;

        }

}

3.2.3 循环单链表

声明结构体类型。

typedef struct {

        int data;

        struct CNode *next;

}CNode,*CirList;

初始化结构体

bool InitCList(CirList L){

        L = (CNode *)malloc(sizeof(CNode));//分配一个头结点

        if(L==NULL) return false;

        L->next = L;// 尾结点指向头结点

        return true;

}

判断循环单链表是否为空

bool Empty(CirList L){

        CNode *p = L;//指向当前节点

        if(p->next=p) return true;

        else

                 return false;

}

判断该节点是否为循环单链表尾结点

bool IsTail(CirList L,CNode *p){

        if(L==NULL||p==NULL) return false;

        if(p->next==L) return true;

        else

                 return false;

}

测试

bool TestCDList(){

        CDList L;

        InitCDList(L);

................

return ture;

}

3.2.4 循环双链表

声明循环单链表结构体类型

typedef struct{

        int data;

        struct CDNode *prior;

        struct CDNode *next;

}CDNode,*CDList;

初始化

bool InitCDList(CDList L){

        L=(CDList)malloc(sizeof(CDNode));

        if(L==NULL) return false;

        L->prior = L;

        L->next = L;

}

判断为空

bool Empty(CDList L){

        if(L->next==L) return true;

        else

                 return false;

}

判断是否是循环双链表的尾结点

bool IsTail(CDList L,CDNode *p){

        if(p->next==L) return true;

        else

                 return false;

}

测试

bool TestCDList(){

        CDList L;

        InitCDList(L);

.....

Return true;

}

插入:

在p结点后插入s结点

bool InsertNextCDNode(CDNode *p,CDNode *s){

        p->next->prior = s;

        s->next = p->next;

        p->next = s;

        s->prior = p;

        return true

}

与普通双链表的区别是,不用考虑p结点的后继结点是NULL的问题。

删除:

删除结点p

bool DeleteCDNode(CDNode *p){

        CDNode *q = p->prior;

数据结构第四篇——线性表的链式存储之双向链表

N日一篇——Java实现链式表

第四篇[机器学习] 机器学习,线性回归的优化

40篇学完C语言——(第四篇)指针与地址

第四篇排序算法|二分查找

C语言从青铜到王者第四篇·详解操作符

(c)2006-2024 SYSTEM All Rights Reserved IT常识