数据结构学习

数据结构

目录

数据结构

前言

一、线性表

1.1 线性表的顺序表示(顺序表)

1.1.1 定义:

1.1.2 顺序表的基本操作实现

1.2 线性表的链式表示

 1.2.1  单链表的定义

1.2.2 单链表的基本操作 

1.2.3 双链表

1.2.4 循环链表 

1.2.5 静态链表

1.3 顺序表 & 链表



前言

提示:学习数据结构,扎实自己的算法基础,提升自己的编程思维


一、线性表

        线性表是具有相同类型的n(n>=0)个元素的有限序列,其中n为表长,当n=0时,线性表是一个空表。用L命名线性表,则其一般表示为:

        L = ( a1,a2,a3,..,ai,ai+1,...,an )

        式中a1是唯一一个“第一个”数据元素,又称为表头元素,an是“最后一个”数据元素,又称表尾元素。除第一个元素之外,每个元素有且只有一个直接前驱,除最后一个元素外,每个元素有且只有一个直接后继。

        线性表的特点:

  1.  表中元素的个数有限。
  2. 表中元素具有逻辑顺序性,表中元素有其先后次序。
  3. 表中元素都是数据元素,每个元素都是单个元素。
  4. 表中元素的数据类型相同,这意味着每个元素占有相同大小的存储空间。
  5. 表中元素具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容。

注意:线性表是一种逻辑结构,表示元素之间一对一的相邻关系,顺序表和链表是指存储结构,两者属于不同层面的概念,不要混淆。

1.1 线性表的顺序表示(顺序表)

1.1.1 定义:

        它是一组地址连续的存储单元依次存储数据表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。

线性表中元素的位序从1开始,而数组中元素的下标是从0开始的。

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

#define MaxSize 50             //定义线性表的最大长度
typedef struct {
    ElemType data[MaxSize];    //顺序表的元素
    int length;                //顺序表的类型定义
}SqList;

以上为线性表静态分配,一旦空间占满,再加入新的数据会产生溢出,进而导致程序崩溃。

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

#define InitSize 100    //表长度的初始定义
typedef struct{
    ElemType *data;    //指示动态分配数组的指针
    int MaxSize,length;    //数组的最大容量和当前个数
}SeqList;    //动态分配数组顺序表的类型定义

C语言的动态初始分配语句:

L.data=(ElemType *)malloc(sizeof(ElemType)*InitSize);

C++的初始动态分配空间语句:

L.data = new ElemType[InitSize];

         动态分配并不是链式存储,它依然是顺序存储结构,不改变物理结构,随机存取,分配的大小空间在运行时决定        

顺序表的特点:

  • 随机访问,通过首地址和元素序号可以在O(1)内找到指定的元素
  • 存储密度高,每个结点只存储数据元素。
  • 逻辑相邻也物理相邻, 所以在插入和删除操作时需要移动大量元素。

1.1.2 顺序表的基本操作实现

1.插入:在顺序表L的i位置(1<=i<=L.length+1)插入新元素e,若i的输入不合法,则返回false,

表示插入失败,否则,将顺序表第i个元素及其后的所有元素右移一个位置,腾出一个空位置插入新元素e,顺序表长度加1,返回值为true.

bool InsertList(SeqList &L, int i, ElemType e) {
    if(i<1 || i> L.length+1) {
        return false;
    }
    if(L.length > MaxSize) {
        return false;
    }
    for(int j = L.length;j>=i;j--) {    //将第i个元素以及之后的元素后移
        L.data[j] = L.data[j-1];
    }
    L.data[i-1] = e;
    L.length++;
    return true;
}
  • 最好情况:在表尾插入,元素后移语句不执行,时间复杂度O(1)
  • 最坏情况:在表头插入,元素后移执行n次,时间复杂度O(n)
  • 平均情况:1/(n+1) * n(n+1)/2 = n/2 ,其复杂度为O(n) 

2.删除

bool DeleteList(SeqList &L, int i, ElemType e) {
    if(i<1 || i>L.length) {
        return false;
    }
    e = L.data[i-1];    //将被删除的元素赋值给e
    for(int j=i;j<L.length;j++) {    //元素前移
        L.data[j-1]=L.data[j];
    }
    L.length--;
    return true;
}
  •  Best:删除表尾元素,无需移动元素,时间复杂度O(1)
  • Worst:删除表头元素,需要移动n-1个元素,时间复杂度为O(n)
  • 平均情况:1/n * n(n-1)/2 = (n-1)/2 时间复杂度:O(n)

 3.查找第一个元素值等于e的元素,返回其顺序

int LocateElem(SeqList L, ElemType e) {
    int i;
    for(i=0;i<L.length;i++) {
        if(L.data[i] == e) {
            return i+1;    // 下标为i的元素值等于e,返回其位序i+1
        }
    }
    return 0;              // 退出循环,说明查找失败
}
  • 最好情况:元素位于表头,只比较一次,时间复杂度为O(1)
  • 最坏情况:查找的元素位于表尾或不存在。需要比较n次,时间复杂度为O(n)
  • 平均情况:1/n * n(n+1)/2 = (n+1) / 2;  O(n) 

1.2 线性表的链式表示

        链式存储线性表,不需要使用地址连续的存储单元(不要求逻辑上相邻的元素在物理位置上也相邻),它通过链建立数据元素之间的逻辑关系,因此插入和删除操作不需要移动元素,而只需要修改指针,但是也失去了顺序表随机存取的优势。

 1.2.1  单链表的定义

typedef struct LNode{
    ElemTpe data;
    struct LNode *next;
}LNode,*LinkList;

1.2.2 单链表的基本操作 

        1.采用头插法建立单链表

        该方法从一个空表开始,生成新的结点,并将读取到的数据放到新节点的数据域中,然后将新节点插入到当前链表的表头,即头结点之后。

LinkList HeadInsert_List(LinkList &L) {    //逆向建立单链表
    LNode *s;                              
    int x;
    L = (LinkList)malloc(sizeof(LNode));    //创建头结点
    L->next = null;                         //初始化为空链表
    scanf("%d",&x);
    while(x!=9999) {                        //输入9999结束
        s = (LNode *)malloc(sizeof(LNode));    //创建新节点
        s->data = x;
        s->next = L->next;
        L->next = s;
        scanf("%d",&x);
    }
    return L;
}

        采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素的顺序时相反的,每个结点插入的时间为O(1),设单链表的总长度为n,则总时间复杂度为O(n)。 

        2 尾插法建立链表

LinkList TailInsertList(LinkList &L) {    //正向建立单链表
    int x;                                //设置元素类型为整型
    L = (LinkList)malloc(sizeof(LNode));
    LNode *s,*r=L;                         //r为表尾指针
    scanf("%d",&x);                        
    while(x!=9999) {                        // 输入9999表示结束
        s=(LNode *)malloc(sizeof(LNode));
        s->data = x;
        r->next = s;
        r = s;                               //r指向新的表尾结点
        scanf("%d",&x);
    }
    r->next = NULL;                           //尾结点指针置空
    return L;
}

         3 按序号查找结点值

在单链表中从第一个结点出发,顺指针next域逐个往下搜索,直到找到第i个结点为止,否则返回最后一个结点指针域NULL。

LNode *GetElem(LinkList L, int i) {
    int j = 1;                            //计数,初始值为1
    LNode *p = L->next;                   //头结点指针赋给p
    if(i==0) {
        retrurn L;
    }
    if(i<1) {
        return NULL;
    }
    while(p&&j<i) {
        p = p->next;
        j = j++;
    }
    return p;
}

时间复杂度:O(n)

         4.按值查找表结点

从单链表的第一个结点开始,由前向后依次比较表中各个结点数据域的值,若某个结点数据域的值等于给定值e,则返回该节点的指针,若整个单链表中没有这样的节点,则返回NULL。

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

       5.插入节点操作

        插入节点操作将值x的新节点插入到单链表的第i个位置上,先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i-1个结点,再在其后插入新结点。

        思路:调用按序号查找方法GetElem(L,i-1),查找第i-1个结点,假设返回的第i-1个结点为*p,然后令新结点 *s 的指针域指向 *p 的后继结点,再令结点 *p 的指针域指向新插入的结点 *s。其操作过程:

p = GetElem(L,i-1);    //查找插入位置的前驱结点
s -> next = p -> next;   
p -> next = s;

        s -> next = p -> next 语句 与 p -> next = s语句不能颠倒,否则,若先执行p->next -s 语句之后,指向其原来后继的指针就不存在,在执行s->next = p->next,相当于执行了 s->next = s,显然是错误的。该算法主要的时间开销在于查找第 i-1 个元素,时间复杂度为 O(n) 。若在给定的结点后面插入新结点,这时间复杂度为 O(1)。

         对某一结点进行前插操作:

        前插操作是指在某一结点的前面插入一个新结点,与后插操作刚好相反,在单链表的操作中,通常采用后插法。

        方法一:O(n)

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

        方法二:将带插入结点插入到p结点的后面,然后将s结点和p结点的数据域互换,实现前插的效果,具体实现如下:

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

 6.删除结点操作

删除结点操作是将单链表的第 i 个结点删除。先检查删除位置的合法性,后查找表中第 i-1 个结点,即被删除的结点的前驱结点,再将其删除,操作过程如下所示:

         *p 为找到的被删除结点的前驱结点,修改 *p 的指针域,即将 *p 的指针域 next 指向 被删除结点 *q的下一个结点。

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

         时间复杂度:O(n)

扩展:删除 *p 结点

        将其后继结点的值赋予自身,然后删除后继结点,然后删除后继结点,因为避免了查找操作,所以使得时间复杂度为O(1)

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

7.求表长操作:计算单链表中数据结点(不含头结点)的个数,需要从第一个结点开始顺序一次访问表中的每个结点,为此需要设置一个计数器变量,每访问一个结点,计数器加1,直到访问到空结点为止,算法的时间复杂度为O(n)。

1.2.3 双链表

        单链表的缺陷:由于单链表只有一个指向后继的指针,使得单链表只能从头结点依次顺序地向后面遍历。要访问某个结点的前驱结点(插入,删除),只能从头开始遍历,访问后继结点的时间复杂度为O(1),访问前驱结点的时间复杂度为O(n)。

        由此,双链表横空出世,其结点有两个指针priot和next,分别指向其前驱结点和后继结点,如图所示

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

 1.插入操作:

        

s->next = p->next;
p->next->priod = s;
s->priod = p;
p->next = s;

 注:1和2必须在4之前完成。

2.删除操作

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

1.2.4 循环链表 

循环单链表:

        循环链表和单链表的区别在于,表中的最后一个节点的指针不是NULL,而是改为指向头结点。

  1. 在循环链表中,表尾结点*r的next域指向L,故表尾没有指针域为NULL的结点,因此,判定循环单链表的判空条件为它是否指向头指针。
  2. 循环单链表在任何一个位置进行插入和删除操作都是等价的。
  3.  对循环单链表设置尾指针的效率更高,因为若是设置头指针,对表为进行操作时间复杂度为O(n),而若是尾指针,r->next即为头指针,对于表头和表尾进行操作的时间复杂度均为O(1)

2.循环双链表

        在循环双链表中,某结点 *p 为尾结点时,p->next ==L; 当循环双链表为空表时,其头结点的piror域和next域都等于L。 

1.2.5 静态链表

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

        静态链表和单链表之间的对应关系:

        

结构类型描述如下:

#define MaxSize 60    //静态链表最大长度
typedef struct{        //静态链表结构类型的定义
    ElemType data;    //存储数据元素
    int next;         //下一个元素的数组下标
}SLinkList[MaxSize];

         静态链表以next == -1 作为其结束的标志,静态链表的插入、删除操作与动态链表相同。只需要修改指针,而不需要移动元素。总体来说,静态链表没有单链表使用方便。

1.3 顺序表 & 链表

顺序表(访问)链表(插入,删除)
存取(读写)方式顺序存取,随机存取,一次访问n次访问,从表头顺序存取元素
逻辑结构与物理结构逻辑上相邻的元素,物理上也相邻

逻辑上相邻的元素物理上比一定相邻,对应的

逻辑关系通过指针连接来表示

查找、插入、删除操作

无须:O(n)

有序(折半查找):O(logn)

O(n)
空间分配         效率低 

结点空间只在需要时申请分配,只要内存有空间

就可以分配,操作灵活高效。

a.基于存储考虑

难以估计线性表的长度或存储规模

时,不宜采用顺序表

不需要实现估计规模,但链表的存储密度较低,显然

其存储密度是小于1的。

b.基于运算的考虑

按序号访问:O(1)

插入删除:平均移动表中一半元素,

O(n)

按序号访问:O(n)

插入删除:优于顺序表

c.基于环境考虑 

顺序表容易实现,因为任何高级语言中都

有数组类型

链表是基于指针的

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值