首页 > 技术文章 > 01*: 数据结构(逻辑结构和物理结构)、算法、时间和空间复杂度 ( 顺序表(查询修改)、线性表(增删))(【O(1)<O(log n)<O(n)<O(nlog n)<O(n^2)<O(n^3)<O(n!)<O(n^n)】)

zyzmlc 2020-05-01 14:01 原文

问题

 

目录

1:数据结构

2:算法

3:顺序表

4:线性表 

预习

正文

一:数据结构

1:数据结构:
是指相互之间存在一种或多种特定关系的数据元素的集合用计算机存储、组织数据的方式。数据结构分别为逻辑结构、(存储)物理结构和数据的运算三个部分。
常见的数据结构有:队列,树,堆,数组,栈,链表,涂,散列表.

2:  数据结构基本数据单位

数据结构中最基本的5个概念: 数据,数据元素,数据项,数据对象,数据结构;

2.1:数据:“是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。数据不仅仅包括整型、实型等数值类型,还包括字符及声音、图像、视频等非数值类型。”

2.2:数据对象:是性质相同的数据元素的集合,是数据的子集. 那么什么叫性质相同? 是指数据元素具有相同数量和类型的数项. 类似数组中的元素保持性质一致.

2.3:数据元素:是组成数据的,且有一定意义的基本单位,在计算机中通常作为整体处理. 也被称作"记录" 例如,我们生活的圈子里.什么叫数据元素了? 人Person ,汽车Car 等.

2.4:数据项:一个数据元素可以由若干数据项组成. 比如,Person 数据元素,可以为分解为眼睛,耳朵,鼻子,嘴巴,手臂这些基本的数据项,也可以从另外的角度拆解成姓名,年龄,性别,出生地址,出生日期,联系电话等数据项. 那么你如何拆解数据项, 要看你的项目来定. 数据项是数据不可分割的最小单位. 在我们的课程中,我们把数据定位为最小单位.这样有助于我们更好理解以及解决问题.

2.5:数据逻辑结构

   集合

   线性结构:队列,栈,数组,字符串,一对一的关系

   树形结构:一对多的关系。

   图形结构:多对多的关系。

2.6:物理存储结构

   顺序存储结构

   链式存储结构

分为:顺序存储结构(内存空间是连续的),链式存储结构(内存空间是不连续的)

 

2.7:数据结构

主要任务是通过分析数据对象的结构特征,包括逻辑结构及数据对象之间的关系,然后把逻辑结构表示成计算机课实现的物理结构,从而便于计算机处理。

2.8:逻辑结构和物理结构的区别

• 逻辑结构:指在数据中数据元素之间的相互关系。分为:线性结构和非线性结构

  1. 线性结构:结构关系是一对一的,并且是一种先后的次序
    • 主要特征为存在唯一的被叫做第一个和最后一个数据,
    • 除第一个元素之外每个数据元素均有⼀个前驱。
    • 除最后一个元素之外每个数据元素均有一个后继。
    • 表现形式为:线性表、栈和队列、字符串(特殊的线性结构)
  2. 非线性结构:
    • 表现形式为: 集合结构、树形结构、图形结构
    • 集合结构: 元素之间没有特殊的关系,只是属于一个集合
    • 树形结构: 元素结构关系是一对多,比如公司的人员架构图( CEO)
    • 图形结构: 结构关系是多对多的,比如铁路网

• 数据的物理结构:数据的逻辑结构在计算机中的存储形式

1.顺序存储结构: 数据元素存放在一组存储地址连续的存储单元里,其数据元素间的逻辑关系和物理关系是一致的
2.链式存储结构: 数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的

二:算法

什么是算法? 算法就是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列, 并且每个指令表示⼀个或多个操作

1:算法特征

• 有穷行: 算法可以在某一个条件下自动结束而不是出现无限循环
• 确定性: 算法执行的每一步骤在一定条件下只有一条执行路径,一个相同的输入对应相同的一个输出
• 可行性: 算法每一步骤都必须可行,能够通过有限的执行次数完成
• 输入: 算法具有零个或多个输入
• 输出: 算法至少有一个或多个输出

时间效率高和储存量低(时间复杂度和空间复杂度)

2:时间复杂度

2.1:大O表示法  

1. 用常数1取代运行时间中所有常数 3->1 O(1)
2. 在修改运行次数函数中,只保留最高阶项 n^3+2n^2+5 -> O(n^3)
3. 如果在最高阶存在且不等于1,则去除这个项目相乘的常数 2n^3 -> n^3

2.2: 时间复杂度术语:

 1. 常数阶 O(1)

2. 线性阶 O(n)

3. 平方阶 O(n^2)

4. 对数阶 O(logn)

5. 立方阶 O(n^3)

6. nlog阶

7. 指数阶(不考虑) O(2^n)或者O(n!) 除非是非常小的n,否则会造成噩梦般的时间消耗. 这是一种不切实际的算法时间复杂度. 一般不考虑!

  

1:常数阶  O(1)

//1+1+1+1+1+1+1 = 7 O(1)
void testSum2(int n){
    int sum = 0;                //执行1次
    sum = (1+n)*n/2;            //执行1次
    sum = (1+n)*n/2;            //执行1次
    sum = (1+n)*n/2;            //执行1次
    sum = (1+n)*n/2;            //执行1次
    sum = (1+n)*n/2;            //执行1次
    printf("testSum2:%d\n",sum);//执行1次
    
}

2:线性阶 O(n)

//1+(n+1)+n+1 = 3+2n -> O(n)
void testSum3(int n){
    int i,sum = 0;               //执行1次
    for (i = 1; i <= n; i++) {   //执行n+1次
        sum += i;                //执行n次
    }
    printf("testSum3:%d\n",sum);  //执行1次
}

3:对数阶 O(log n)

/*3.对数阶*/
/*2的x次方等于n x = log2n  ->O(logn)*/
void testA(int n){
    int count = 1;         //执行1次
    //n = 10
    while (count < n) {
        count = count * 2;
    }
}

4:平方阶 O(n^2)

/*4.平方阶*/
//x=x+1; 执行n*n次 ->O(n^2)
void add3(int x,int n){
    for (int i = 0; i< n; i++) {
        for (int j = 0; j < n ; j++) {
            x=x+1;
        }
    }
}
//1+(n+1)+n(n+1)+n^2+n^2 = 2+3n^2+2n -> O(n^2)
void testSum5(int n){
    int i,j,x=0,sum = 0;           //执行1次
    for (i = 1; i <= n; i++) {     //执行n+1次
        for (j = 1; j <= n; j++) { //执行n(n+1)
            x++;                   //执行n*n次
            sum = sum + x;         //执行n*n次
        }
    }
    printf("testSum5:%d\n",sum);
}

5. 立方阶 O(n^3)

/*5.立方阶*/
void testB(int n){
    int sum = 1;                         //执行1次
    for (int i = 0; i < n; i++) {        //执行n次
        for (int j = 0 ; j < n; j++) {   //执行n*n次
            for (int k = 0; k < n; k++) {//执行n*n*n次
                sum = sum * 2;          //执行n*n*n次
            }
        }
    }
}

3:空间复杂度

程序空间计算因素:

 1. 寄存本身的指令

 2. 常数

 3. 变量

 4. 输入

 5. 对数据进行操作的辅助空间

 在考量算法的空间复杂度,主要考虑算法执行时所需要的辅助空间.

 空间复杂度计算:

 //算法实现(1)  O(1)
    int temp;
    for(int i = 0; i < n/2 ; i++){
        temp = a[i];
        a[i] = a[n-i-1];
        a[n-i-1] = temp;
    }

    for(int i = 0;i < 10;i++)
    {
        printf("%d\n",a[i]);

    }
    
    //算法实现(2) O(n)
    int b[10] = {0};
    for(int i = 0; i < n;i++){
        b[i] = a[n-i-1];
    }
    for(int i = 0; i < n; i++){
        a[i] = b[i];
    }
    for(int i = 0;i < 10;i++)
    {
        printf("%d\n",a[i]);
        
    }

三:线性表

1: 概念

线性表是一种简单的线性结构,特点是在非空的有限集合中,且第一个元素没有直接前驱元素,最后一个元素没有直接后继元素,其他元素都有唯一的前驱和后继元素。线性表有顺序存储结构和链式存储结构。
总结一下就是:
对于⾮空的线性表和线性结构,其特点如下:
• 存在唯⼀的⼀个被称作”第⼀个”的数据元素;
• 存在唯⼀的⼀个被称作”最后⼀个"的数据元素
• 除了第⼀个之外,结构中的每个数据元素均有⼀个前驱
• 除了最后⼀个之外,结构中的每个数据元素都有⼀个后继

2:顺序存储结构

是指将线性表中的各个元素依次存放在一组地址连续的存储单元中,通常将这种方法存储的线性表称为顺序表。

假设,线性表的每个元素需占用m个存储单元,并以所占的第一个单元的存储地址作为数据元素的存储位置。则线性表中第i+1个元素的存储位置location(ai+1)和第i个元素的存储位置location(ai)之间满足关系location(ai+1)=location(ai)+m。线性表中第i个元素的存储位置与第一个元素的a1的存储位置满足以下关系,location(ai) =location(a1)+(i-1)*m。其中,第一个元素的位置location(a1)称为起始地址或基地址。

顺序表逻辑上相邻的元素在物理上也是相邻的。每一个数据元素的存储位置都和线性表的起始位置相差一个和数据元素在线性表中的位序成正比的常数。只要确定了第一个元素的起始位置,线性表中的任一个元素都可以随机存取,因此,线性表的顺序存储结构是一种随机存取的存储结构。

顺序表存储数据时,会提前申请一块足够大的屋里空间,然后将数据依次存储起来,存储时做到数据之间不留一丝缝隙。

{1,2,3,4,5}对应的存储状态如下图:

线性表-关于顺序存储的实现(增删改查)

线性表:一对一的逻辑结构

线性表的顺序结构--数组

3:代码

1 顺序表初始化

#define MAXSIZE 100
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

typedef int ElemType;
typedef int Status;
typedef struct {
    ElemType *data;
    int length
}Sqlist;

//1.1 顺序表初始化
Status InitList(Sqlist *L){
    L->data = malloc(sizeof(ElemType) * MAXSIZE);
    if(!L->data) exit(ERROR);
    L->length = 0;
    return OK;
}

2 顺序表的插入

//1.2 顺序表的插入
Status ListInsert(Sqlist *L,int i,ElemType e){
    if((i<1) || (i>L->length+1)) return ERROR;
    if(L->length == MAXSIZE) return ERROR;
    if(i <= L->length){
        for(int j = L->length-1;j>=i-1;j--){
            //插入位置以及之后的位置后移动1位
            L->data[j+1] = L->data[j];
        }
    }
    
    L->data[i-1] = e;
    ++ L->length;
    return OK;
}

3 顺序表的取值

//1.3 顺序表的取值
Status GetElem(Sqlist L,int i, ElemType *e){
    if(i<1 || i > L.length) return  ERROR;
    *e = L.data[i-1];
    return OK;
}

4 顺序表删除

//1.4 顺序表删除
Status ListDelete(Sqlist *L,int i){
    if(L->length == 0) return ERROR;
    if((i<1) || (i>L->length+1)) return ERROR;
    for(int j = i; j < L->length;j++){
        //被删除元素之后的元素向前移动
        L->data[j-1] = L->data[j];
    }
    L->length --;    
    return OK;
}

5 清空顺序表

//1.5 清空顺序表
Status ClearList(Sqlist *L){
    L->length=0;
    return OK;
}
//1.6 判断顺序表清空
Status ListEmpty(Sqlist L){
    if(L.length==0)
        return TRUE;
    else
        return FALSE;
}
//1.7 获取顺序表长度ListLength元素个数
int ListLength(Sqlist L){
    return L.length;
}

6 顺序输出List

//1.8 顺序输出List
Status TraverseList(Sqlist L){
    int i;
    for(i=0;i<L.length;i++)
        printf("%d\n",L.data[i]);
    printf("\n");
    return OK;
}

7 顺序表查找元素并返回位置

//1.9 顺序表查找元素并返回位置
int LocateElem(Sqlist L,ElemType e){
    int i;
    if (L.length==0) return 0;
    
    for(i=0;i<L.length;i++){
        if (L.data[i]==e)
            break;
    }
  
    if(i>=L.length) return 0;
    return i+1;
}

8 调用

int main(int argc, const char * argv[]) {
    printf("Hello, Data Structure!\n");
    
    Sqlist L;
    Sqlist Lb;
    ElemType e;
    Status iStatus;
    
    iStatus = InitList(&L);
    
    for(int j=1; j <= 5;j++){
        iStatus = ListInsert(&L, 1, j);
    }
    
    GetElem(L, 5, &e);
    ListDelete(&L, 2);
    iStatus = ClearList(&L);
    iStatus=ListEmpty(L);
    
    for(int j=1; j <= 5;j++){
        iStatus = ListInsert(&L, 1, j);
    }
    TraverseList(L);
    
    return 0;
}

四:线性表-链式存储

链表的定义是递归的,它或者为空null,或者指向另一个节点node的引用,这个节点含有下一个节点或链表的引用。

与顺序存储相比,允许存储空间不连续,插入删除时不需要移动大量的元素,只需修改指针即可,但查找某个元素,只能从头遍历整个链表。

首元结点:指链表中存储线性表中第一个数据元素的结点,是链表的开始结点。

头指针:指向链表中第一个结点(或为头结点或为首元结点)的指针

头结点:头结点是在链表的首元结点之前附设的一个结点,数值域可不设任何信息(也可根据需求保存信息,如链表的长度等),头结点的指针域指向链表的第一个元素。

单链表分为带头结点不带头结点两种,不管有没有头结点,头指针都指向链表的第一个节点(有头结点指向头结点)。

带头节点的好处有:

  • 1.链表第一位置节点上的操作和其它位置上的操作一致(便于首元结点处理理)
  • 2.无论链表是否为空,头指针都指向头结点,空表和非空表处理一样(便于空表和非空表的统一处理)
1:结点

 

2:单链表的头结点
没有头节点,首指针指向首元结点

 

有头节点,首指针指向头节点
 

注意:链表麻烦的地方是插入和删除时指针的修改,保证不断链,一般先链后断。

1 初始化单链表线性表

#define ERROR 0
#define TRUE 1
#define FALSE 0
#define OK 1

#define MAXSIZE 20 /* 存储空间初始分配量 */

typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */

//定义结点
typedef struct Node{
    ElemType data;
    struct Node *next;
}Node;

typedef struct Node * LinkList;

2 单链表插入

插入位置为首结点

  • 创建目标结点
  • 遍历链表,找到尾结点
  • 目标结点next指针指向首结点*L
  • 首结点指向目标结点
  • 尾结点next指针指向首结点

插入位置为其他结点

  • 创建目标结点
  • 遍历链表,找到目标位置的前一个结点target
  • 目标结点next指针指向targetnext结点。
  • targetnext指针指向目标结点
/*
 初始条件:顺序线性表L已存在,1≤i≤ListLength(L);
 操作结果:在L中第i个位置之后插入新的数据元素e,L的长度加1;
 */
Status ListInsert(LinkList *L,int i,ElemType e){
 
    int j;
    LinkList p,s;//p是插入位置前一个node, s是要插入的node
    p = *L;
    j = 1;
    
    //寻找第i-1个结点  0>1>2>3>4>5 i = 3
    while (p && j<i) {
        p = p->next;
        ++j;
    }
    
    //第i个元素不存在
    if(!p || j>i) return ERROR;
    
    //生成新结点s
    s = (LinkList)malloc(sizeof(Node));
    //将e赋值给s的数值域
    s->data = e;
    //
![](https://user-gold-cdn.xitu.io/2020/4/1/171349562f080e26?w=2934&h=994&f=jpeg&s=72114)将p的后继结点赋值给s的后继
    s->next = p->next;
    //将s赋值给p的后继
    p->next = s;
    
    return OK;
}

3 单链表删除元素

 

  1. 删除的位置为首结点
  • 如果首结点next指向首结点,即链表只剩一个结点,释放首结点,指针置NULL
  • 遍历链表,找到尾结点
  • 临时变量temp记录*L
  • *L指向*Lnext
  • 尾结点next指向*L
  • 释放temp
  1. 删除的位置为其他结点
  • 遍历链表,找到目标结点的前一个结点target
  • 临时变量temp记录目标结点
  • target的next指向目标结点next
  • 释放temp
/*
 初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1
 */

Status ListDelete(LinkList *L,int i,ElemType *e){
    
    int j;
    LinkList p,q;//p是插入位置前一个node, q是要删除的node
    p = (*L)->next;
    j = 1;
    
    //查找第i-1个结点,p指向该结点
    while (p->next && j<(i-1)) {
        p = p->next;
        ++j;
    }
    
    //当i>n 或者 i<1 时,删除位置不合理
    if (!(p->next) || (j>i-1)) return  ERROR;
    
    //q指向要删除的结点
    q = p->next;
    //将q的后继赋值给p的后继
    p->next = q->next;
    //将q结点中的数据给e
    *e = q->data;
    //让系统回收此结点,释放内存;
    free(q);
    
    return OK;
}

4 单链表取值

/*
 初始条件: 顺序线性表L已存在,1≤i≤ListLength(L);
 操作结果:用e返回L中第i个数据元素的值
 */
Status GetElem(LinkList L,int i,ElemType *e){
    
    //j: 计数.
    int j;
    //声明结点p;
    LinkList p;
    
    //将结点p 指向链表L的第一个结点;
    p = L->next;
    //j计算=1;
    j = 1;
    
    
    //p不为空,且计算j不等于i,则循环继续
    while (p && j<i) {
        
        //p指向下一个结点
        p = p->next;
        ++j;
    }
    
    //如果p为空或者j>i,则返回error
    if(!p || j > i) return ERROR;
    
    //e = p所指的结点的data
    *e = p->data;
    return OK;
}

5 单链表元素输出

/* 初始条件:顺序线性表L已存在 */
/* 操作结果:依次对L的每个数据元素输出 */
Status ListTraverse(LinkList L)
{
    LinkList p=L->next;
    while(p)
    {
        printf("%d\n",p->data);
        p=p->next;
    }
    printf("\n");
    return OK;
}

6 单链表清空

/* 初始条件:顺序线性表L已存在。操作结果:将L重置为空表 */
Status ClearList(LinkList *L)
{
    LinkList p,q;
    p=(*L)->next;           /*  p指向第一个结点 */
    while(p)                /*  没到表尾 */
    {
        q=p->next;
        free(p);
        p=q;
    }
    (*L)->next=NULL;        /* 头结点指针域为空 */
    return OK;
}

7 单链表前插入法 (头插法)

将新节点插入到当前链表的表头,(头结点之后),插入的顺序与链表中的顺序相反,关键点就是记住旧的表头,生成一个新的放到旧表头前面

/* 随机产生n个元素值,建立带表头结点的单链线性表L(前插法)*/
void CreateListHead(LinkList *L, int n){
    
    LinkList p;
    
    //建立1个带头结点的单链表
    *L = (LinkList)malloc(sizeof(Node));
    (*L)->next = NULL;
    
    //循环前插入随机数据
    for(int i = 0; i < n;i++)
    {
        //生成新结点
        p = (LinkList)malloc(sizeof(Node));
       
        //i赋值给新结点的data
        p->data = i;
        //p->next = 头结点的L->next (上一条数据)
        p->next = (*L)->next;
        
        //将结点P插入到头结点之后;
        (*L)->next = p;
        
    }
}

8 单链表后插入法 (尾插发)

增加一个尾指针,新节点插到链表的尾部,插入的顺序和链表的顺序一致

/* 随机产生n个元素值,建立带表头结点的单链线性表L(后插法)*/
void CreateListTail(LinkList *L, int n){
    
    LinkList p,r;
 
    //建立1个带头结点的单链表
    *L = (LinkList)malloc(sizeof(Node));
    //r指向尾部的结点
    r = *L;
    
    for (int i=0; i<n; i++) {
        
        //生成新结点
        p = (Node *)malloc(sizeof(Node));
        p->data = i;
        
        //将表尾终端结点的指针指向新结点
        r->next = p;
        //将当前的新结点定义为表尾终端结点
        r = p;
    }
    
    //将尾指针的next = null
    r->next = NULL;
}

9 test

Status iStatus;
    LinkList L1,L;
    struct Node *L2;
    ElemType e; 
    
    //2.1 单链表初始化
    iStatus = InitList(&L);
    printf("L 是否初始化成功?(0:失败,1:成功) %d\n",iStatus);
    
    //2.2 单链表插入数据
    for(int j = 1;j<=10;j++)
    {
        iStatus = ListInsert(&L, 1, j);
    }
    printf("L 插入后\n");
    ListTraverse(L);
    
    //2.3 单链表获取元素
    GetElem(L,5,&e);
    printf("第5个元素的值为:%d\n",e);
    
    //2.4 删除第5个元素
    iStatus = ListDelete(&L, 5, &e);
    printf("删除第5个元素值为:%d\n",e);
    ListTraverse(L);
    
    //3.1 前插法整理创建链表L
    iStatus = ClearList(&L);
    CreateListHead(&L, 20);
    printf("整理创建L的元素(前插法):\n");
    ListTraverse(L);
    
    //3.2 后插法整理创建链表L
    iStatus = ClearList(&L);
    CreateListTail(&L, 20);
    printf("整理创建L的元素(后插法):\n");
    ListTraverse(L);

结论

1:线性表-顺序表:主要是值的位置和length的改变。

2:线性表-单链表:主要是指针域的改变,

前插法:主要是把插入结点放到首节点的位置。

尾插法:主要是把插入结点放到尾部结点。

引用:

1:数据结构与算法-初步认识

2: 数据结构与算法第一讲:[基础+线性表]

3:数据结构与算法学习 线性表

4:数据结构与算法---线性表

5:数据结构-线性表的顺序存储和链式存储-Day2

推荐阅读