数据结-线性表

线性表

线性表描述

在现实的应用中,有两种实现线性表数据元素存储功能的方法

  1. 顺序表存储结构
  2. 链式存储结构

线性表的特性

线性表是一种最基本,最简单的常用数据结构,实际中,线性表都是以List ,stack,queue ,arr ,string等特殊的表现形式来使用

线性表是一个线性结构,他是一个含有n ≥ 0 节点的有限序列. (不过可以想到) 最小位没有前驱(但是有后驱),最后位没有后驱(但有前驱节点)

   k1 ,k2 ,kn...

   特征 : 

              必须存在 唯一的首位元素和唯一的最后元素

              除最后的元素外,必须有唯一后继, 除第一元素外都必须要有唯一前驱

线性表基本操作过程

  Setnull (L)  //: 清空

  Length(L) //:表长度和各元素个数

  Get(L,I)  //获取第i 个元素

  Next(L,I) //获取后继元素

  Locate(L,x)// 返回指定元素位置。

  Insert(L,i,x); //插入元素

  delete(L,x) ;//删除元素

  empty(L); //是否为null

 


线性表的结构特定

 均匀性:虽不同的数据表的数据元素各种各样,但统一线性表元素必须有相同类型和长度

 有序性:数据元素在线性表中的位置只取决于序,数据元素之前的相对位子上线性的,即存在唯一last[i] ,firt[i] 数据元素,出首尾元素外,其他元素只有一个数据源前趋,后面只有一个直接后继.

总结

        由于顺序表的硬性规定,即用连续的存储单元 顺序存储 线性表中的各个元素,所以当对顺序表进行插入和删除时,必须必须移动数据才能实现线性表逻辑上的相邻关系.极大损耗性能.

   .net core 中以 线性表实现的 List 就是非常典型的特点 ,不适合remove 等操作 ,但访问元素的速度非常快.

值得看的是微软在源码中实现线性表最重要的动态扩容操作时 ,无论哪种os下,扩充直接 Capacity*2. 

 

链表 01

问题:

我认为除去效率问题,其实linkedlist最重要的特性
是插入删除的时候迭代器不失效,
可以安全的存储在其他数据结构中。
要牢记比起效率,正确性总是第一位的。

所以很多答案提到了内存池,lru等等,其实关键问题在于哈希表中存储了指向链表元素的指针。
这种程序中一旦分配完句柄,指针就泄露到了容器外部(比如hashmap存储了pageNode)。
如果采用vector那种连续内存空间,一个insert腾挪,指针错位,程序就错了。

至于效率问题,LinkedList本身对缓存的友好程度就不如vector。
在不要求元素排列顺序的情况下,
vector可以把元素和rbegin(反向)进行交换再pop_back(),擦除
照样是常数时间,还更缓存友好,大概率比list快。

如果你去看memcache/linux的slab搞法,
就会发现它其实介于线性数组和链表之间,兼顾了正确性和效率。

链表描述:

     内存中内部的存储方式,通常情况下可以认为是多个节点存储一串的的结构

  链表存储结构

           数据域 Data field

          指针域 pointer field   
强调: 指针域 一般指向下一个节点的地址 , 或者也可以认为是下一个节点的的引用(引用可以指向任意的对象)
指针域实现→ c 内存中的地址 ,地址唯一标记节点的位置
                       存的是数组下标 →  存储的是相对的地址的概念 
               下一个节点,也有存储引用

      只要相关结构中增加了一项指针域,结构就可以串成链表的结构


链表特点:

  

 重点: 正常的链表的LastIndex 是NULL 

 至少必须包含两个部分: 数据和指针域

 链表的每个节点,通过指针域的值,形成了一个线性结构

 ps 由于是通过指针区串联成的一串,所以只允许从前向后依次访问节点. 不允许用下标.

  (访问速度)也就成了o(n ) 

 —也正是链表是一种松散的结构,由于指针域存储的是下一个引用,也就是说我只要改变了指针的指向. 就可以完成动态的插入或修改(我可以吧插入地址,修改成修改前的下一个内存地址)

 插入和修改删除都是o(1)的..

 并不适合快速查找.

链表的实现

指针版

 struct linkListNode{
       linkListNode(int data):data(data),next(NULL){};
       int data;
       linkListNode *next;
    };
    
    void  impleLinkList_bystruct(){
       void  Data_struct::impleLinkList_bystruct(){
       linkListNode *head=NULL;
       head=new linkListNode(1);
       head->next =new linkListNode(2);
       head->next->next = linkListNode(3);
       head->next->next->next =new  linkListNode(4);

       LinkListNode  *p=head;
       while (p!=NULL) {
           cout << p->next << e
        }
       cout << endl;
     }

 

数组下标

    add 函数的作用地址index 节点后 添加一个地址为p的节点,并在让p节点中存储val

    int next[10];
    int data[10];
    void  LinkListadd(int ind ,int p,int val){
       next[ind]=p;
       data[p]=val;
       return ;
   }
    
    void  impleLinkList_byarr(){
         int  head =3 ;
          data[3]=0;
        //头节点是3
        //3节点后添加5节点,存的是1
        LinkListadd(3,5,1);
        //五节点添加2节点,存的是2
        LinkListadd(5,2,2);
        //2节点添加的是7节点 ,存的是3
        LinkListadd(2,7,3);
        // 第二个是下一个节点 ,最后一个是值
        LinkListadd(7,9,100);
        //构造了链表
        
        int p =head;
        while(p=!0){
            cout << "->" <<data[p] << endl;
            p =next[p];
        }
        return 0;
     }

 

  

顺序表02

顺序表是在内存中以数据的形式保存的线性表,指一组地址连续的存单元依次存储数据元素的线性结构,
所以使得线性表的逻辑结构上相邻的数据元素存储在相邻的物理存储单元,即通过数据源物理存储的相邻关系来反应数据元素之间上的逻辑上的相邻关系

  与数组的区别 数组在变以前就就必须确定数组的长度,一旦确认,大小不允许更改. 

  顺序表: 动态开辟的数组大小且可以动态扩容 存储类型必须一致, 需要一段连续的地址空间存储

数据结构

存储结构定义

以下定义两种数据结构 将int 替换为 ElemTye 当做类型传入即可

/* c2-1.h 线性表的动态分配顺序存储结构 */
#define LIST_INIT_SIZE 10 /* 线性表存储空间的初始分配量 */
#define LIST_INCREMENT 2 /* 线性表存储空间的分配增量 */
typedef struct
{
  ElemType *elem; /* 存储空间基址 */
  int length; /* 当前长度 */
  int listsize; /* 当前分配的存储容量(以sizeof(ElemType)为单位) */
}SqList; 

 

typedef  struct seqVector{
  //需要连续的存储空间
   int *data;//连续的存储空间的首地址 他是没有空间的 只是一个指针变量 所需要单独申请
   int size ,length; //size为总长度  当前空间存在的个数
}seqVector;
 seqVector * init(int n){
   //申请存结构变量空间
   seqVector *v=(seqVector *)malloc(sizeof(seqVector));   //向内存申请空间并强转类型
    //动态申请空间 内存堆申请 虽然是函数内部 栈区为8mb  堆区不受8mb限制  *****和loc相关的- malloc 需要主动释放
    //free 释放   需要接受地址 address
   v->data=(int *) malloc(sizeof(int)* n); //动态申请内存区 主动申请空间传递给这个字段
   v->size=n;
   v->length=0;
   return v;
   //自此空间和字段 init 完成
}

 

顺序表:实现

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
#include<pthread.h>
#include<time.h>

//结构定义 
typedef  struct seqVector{
  //需要连续的存储空间
   int *data;//连续的存储空间的首地址 他是没有空间的 只是一个指针变量 所需要单独申请
   int size ,length; //size为总长度  当前空间存在的个数
}seqVector;


//这个容量大小 为n的存储空间
seqVector * init(int n){
   //申请存结构变量空间
   seqVector *v=(seqVector *)malloc(sizeof(seqVector));   //向内存申请空间并强转类型
      //动态申请空间 内存堆申请 虽然是函数内部 栈区为8mb  堆区不受8mb限制  *****和loc相关的- malloc 需要主动释放
    //free 释放   需要接受地址 address
   v->data=(int *) malloc(sizeof(int)* n); //动态申请内存区 主动申请空间传递给这个字段
   v->size=n;
   v->length=0;
   return v;
   //自此空间和字段 init 完成
}


//2 销毁操作 释放内存空间
void clear(seqVector *v){
    if(v==NULL) return;
    free(v->data);
    free(v); //如果你只释放v  只是释放顺序表
    //如 你不按照先后顺序不销毁
    //但是数组还存在,而代码无法访问v-data了
    //你整个内存泄漏了 内存实际是找不到了 os 也找不到 ,你也找不到
    return ;
}
//3 插入
int insert(seqVector *v, int index,int value){
    if(v==NULL){
        //初始化失败了
        return 0;
    }
    if(index <0 || index > v->length){
        return 0;
    
    }
    //这就是满了
    if(v->length==v->size){
        return 0;
    }
    //先把index 空出来 向后移动
   /*
    for(int i=index;i<=v->length;i++){
        // v->data[i+1]=v->data[i];//这特么是覆盖了  全覆盖成了当前index 的值了
    }
   */
    //移动一定要从后往前
    
    for(int i=v->length;i>index;i--){
        //由于他是大于index的 所以我减一是能访到插入点的索引的
        v->data[i]=v->data[i-1];//这才是后移动
    }
    v->data[index]=value;
    v->length+=1;
    return 1;
}

int erase(seqVector *v,int index){
  if(v==NULL) return 0;
  if(index < 0 || index>=v->length)return 0 ;
  for(int i=index +1 ;i< v->length;i++){
      
      v->data[i-1]=v->data[i];
      //从前向后移动
  
  }
  v->length-=1;
  return 1;
}
//结构操作
// 操作步骤
// 1 : 初始化  生成一个顺序表
// 2 : 销毁
// 3 : 插入
// 4:  删除
// 查询
int main()
{
    srand(time(0));
    #define MAX_OP 20
    //必须初始化随机种子
    seqVector *sq =init(MAX_OP);
    for(int i=0;i<MAX_OP;i++){
        int operators=rand()%2; //要么0 要1
        int value=rand()%100;
        int index =rand()%(sq->length+1);
     
        switch(operators){
           case 0:{
                    printf("inert %d at %d to vector%d\n",value,index,insert(sq,index,value));
                  } break;
           case 1:{
                    printf("delete %d at %d form vector ",index,erase(sq,index));
                  }
        }
    }
    
    return 0;
}

 

核心操作动态扩容数组上界

int expand(seqVector *v){
    //三种动态申请空间   
    //malloc ->只是申请空间 不能确定是否初始化
    //calloc ->他可以直接初始化 主动清空为0值
    //realloc ->重新分配内存 
    v->data=(int *)realloc(v->data ,sizeof(int)* (v->size *2));
    v->size  *=2; //  *=2;
    return 1;
}

当前如果申请扩容数组的方法 一旦强转出现问题 则整个数据地址都会为NULL , 会造成代码无法对数据区域进行访问,则整个内存无法呗os 回收,内存出现泄漏.

最好不要在原来的内存空间上直接操作,我们需要将开辟返回后的值赋值给新的变量 

seqList 的理解,我们希望在add方法在像末尾取add元素占用常数时间 . 因此增加n个元素应该是线性的.

 

 .net 中以 线性表实现的 List 就是非常典型的特点 ,不适合remove 等操作 ,但访问元素的速度非常快.

  但是也正是链表是一种松散的结构,由于指针域存储的是下一个引用,也就是说我只要改变了指针的指向. 就可以完成动态的插入或修改(我可以吧插入地址,修改成修改前的下一个内存地址)

插入和修改删除都是o(1)的..

 

 
posted @ 2022-12-14 14:00  Aquiet  阅读(80)  评论(0编辑  收藏  举报