美文网首页
数据结构与算法---线性表

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

作者: 烟火_jason | 来源:发表于2020-11-16 09:15 被阅读0次

    前言

    这篇文章会介绍线性表的内容,其中线性表是1对1的逻辑结构,分别有 顺序表单链表单向循环链表双向链表双向循环链表,接下来介绍的代码都是用c语言的。

    1. 顺序表

    对于非空的线性表和线性结构,其特点如下:

    • 存在唯一一个被称作“第一个”的数据元素。
    • 存在唯一一个被称作"最后一个"的数据元素。
    • 除了第一个之外,结构中的每一个数据元素均有一个前驱。
    • 除了最后一个之外,结构中的每一个数据元素均有一个后继。

    接下来就是介绍线性表的顺序存储,简单来说就是一个数组开辟一段连续的空间,其中顺序存储的逻辑相邻,物理存储地址相邻。

    1.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;//长度
    } Sqlite;
    

    1.2顺序表的操作

    因为顺序表其实就是一个数组,在初始化的时候就开辟好一定的空间,在这个过程中空间的大小是一直存在的。

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

    接下来就是对顺序表的增删改查等操作了。

    • 顺序表的插入数据首先需要判断线性表是否已经存在,长度是否已经满了,插入的位置是否合法(连续插入数据不可以跳跃下标),还要找到位置如果不是表尾的还要进行位移数据(从后往前移动),再修改length
    • 顺序表的值全部删除,其实不用清空清空数据,只需要修改length为0就可以,因为空间大小是一开始就开辟好了,长度是以length为标准的;但是删除某个下标的值就需要先判断当前这个顺序表是否有值并且下标是否合法,再将需要删除的下标的后面的数据向前移动覆盖就可以(从后往前移动数据)
    • 获取某个下标的值,先判断顺序表是否有值,并且当前的下标是否合法
    • 遍历顺序表,先判断顺序表是否合法,通过length值来做遍历的长度。
    //为顺序表插入数据
    Status insertList(Sqlite *L,int index,ElemType e){
        if(index < 0 || (index > L->length && L->length > 0) || L->length >= MAXSIZE){
            return ERROR;
        }
        if(index < L->length){
            for (int j = L->length - 1; j >= index; j -- ) {
                L->data[j+1] = L->data[j];
            }
        }
        L->data[index] = e;
        L->length ++;
        return OK;
    }
    
    //删除某个下标的值
    Status deleteListForIndex(Sqlite *L,int index){
        if(L->length == 0 || index >= L->length){
            return ERROR;
        }
        
        for (int j = index; j < L->length; j++) {
            L->data[j] = L->data[j+1];
        }
        L->length --;
        return OK;
    }
    
    //清空顺序表
    Status clearList(Sqlite *L){
        L->length = 0;
        return OK;
    }
    
    //获取顺序表的长度
    int getListLength(Sqlite L){
        return L.length;
    }
    
    //获取某个下标的值
    Status getListElem(Sqlite L,int i,ElemType *e){
        if(L.length <= 0 || i >= L.length){
            return ERROR;
        }
        *e = L.data[i];
        
        return OK;
    }
    
    //遍历顺序表
    Status traverList(Sqlite L){
        if(!L.data || L.length == 0){
            return ERROR;
        }
        for (int i = 0; i < L.length; i++) {
            printf("%d\t",L.data[i]);
        }
        printf("\n");
        return OK;
    }
    

    顺序表就暂时介绍到这里。

    2.单链表

    单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素数据元素的映象 +指针(指示后继元素存储位置),元素数据域就是存储数据的存储单元,指针就是连接每个结点的地址数据。

    image.png
    线性表的链式存储的最大的特点就是:数据与数据之间是不连续的,通过指针域来连接的。单链表的第一个结点叫首元结点,最后一个结点的指针域为空。在单链表中为了方便对其进行增加和删除的操作更加方便,一般会设置一个头结点。而这个头结点可以方便对首元结点的操作以及对空表和非空表的统一处理。并且这个头结点是在初始化的时候就创建了。

    2.1单链表的操作

    首先需要先定义好,单链表的数据结构。这个第一部分的顺序表的定义是有点区别的,其中ElemType data是数据域,struct Node *next是指针域。

    typedef struct Node{
        ElemType data;
        struct Node *next;
    } Node;
    
    typedef Node *LinkList;
    

    接下来就是初始化单链表,其中在初始化的时候就将头结点初始化出来了

    //初始化
    Status initLinkList(LinkList *L){
        //这个是初始化头结点
        *L = (LinkList)malloc(sizeof(Node));
        if(*L == NULL){
            return ERROR;
        }
        (*L)->next = NULL;
        return OK;
    }
    

    对单链表的数据插入:就是在链表的某个位置插入一条数据。首先要先找到需要插入的结点p,新插入的数据结点为s,先将s的next设置为结点p的下一个结点(p->next),然后再将结点p的next结点指向s。这样就完成了插入。

    //插入
    Status insertLinkList(LinkList *L,int index,ElemType e){
        
        int j = 1;//这个是计数用的,有头结点,就从1开始
        LinkList p,s;
        p = *L;
        while (p && j < index) {
            p = p -> next;
            j ++;
        }
        
        if(!p || j > index){
            return ERROR;
        }
        //定义需要插入的结点
        s = (LinkList)malloc(sizeof(Node));
        s->data = e;
        s->next = p->next;
        p->next = s;
        return OK;
    }
    

    单链表的遍历是最简单的,但是在遍历的时候需要注意的是,我们这个单链接是设置了一个头结点的,在遍历的时候不需要将头结点遍历出来的

    //遍历单链表
    Status ListTraverse(LinkList L){
        LinkList p = L->next;
        while (p) {
            printf("%d\t",p->data);
            p = p -> next;
        }
        printf("\n");
        return OK;
    }
    

    单链表的删除:就是删除指定位置的结点。也是首先找到需要删除的结点的前驱结点q,然后将需要删除的结点用一个临时指针变量s存放,将s的next指针指向q的next,然后再释放结点s。

    //删除的结点,并将值返回
    Status deleteLinkListItem(LinkList *L,int index,ElemType *e){
        LinkList s,q;
        int j = 1;
        q = (*L)->next;
        while (q && j < (index - 1)) {
            q = q->next;
            j ++;
        }
        
        if(!q || j > (index - 1)){
            return ERROR;
        }
        
        s = q->next;//需要删除的结点
        q->next = s->next;
        *e = s->data;
        free(s);
        return OK;
    }
    

    2.2单链表的头插法和尾插法的建立

    头插法:将链表的左边称为链头,右边称为链尾。头插法建单链表是将链表右端看成固定的,链表不断向左延伸而得到的。头插法最先得到的是尾结点。

    //头插法(创建一个有头结点的单链表)
    void createLinkListHead(LinkList *L,int length){
        LinkList p;
        *L = (LinkList)malloc(sizeof(Node));
        (*L)->next = NULL;
        for (int i = 0; i < length; i ++) {
            p = (LinkList)malloc(sizeof(Node));
            p->data = i;
            p->next = (*L)->next;
            (*L)->next = p;
        }
    }
    

    尾插法:若将链表的左端固定,链表不断向右延伸,这种建立链表的方法称为尾插法。尾插法建立链表时,头指针固定不动,故必须设立一个搜索指针,向链表右边延伸,尾插法最先得到的是头结点。

    //后插法 (创建一个有头结点的单链表)
    void createLinkListTail(LinkList *L,int length){
        LinkList p,r;
        *L = (LinkList)malloc(sizeof(Node));
        r = *L;
        for (int i = 0; i < length; i ++) {
            p = (LinkList)malloc(sizeof(Node));
            p->data = i;
            r->next = p;
            r = p;
        }
        r->next = NULL;
    }
    

    3.单向循环链表

    单向循环链表单链表的区别就是:

    • 单链表为了方便对结点的操作会添加一个辅助功能的头结点,并且尾结点的指针域为NULL头结点是在创建的时候就初始化好了。
    • 单向循环链表就没有了头结点,并且尾结点的指针域是指向首结点的,形成一个闭环。如果是初始化一个空表的话,首尾是互相指向的,如图:
      image.png

    3.1单向循环链表的操作

    定义的准备工作还是和单链表的一样。创建单向循环链表并且初始化数据,在创建的过程中有两种情况

    • 初始创建的时候,首元结点的指针域是指向本身的。
    • 有数据的时候创建(新增),尾结点指向首元结点,此时是怎么区别开首元结点呢?就是在遍历的过程中,首元结点L是不动的,遍历过程中的target结点不等于 L情况下去找到尾结点。
    //创建单向循环链表
    void createLinkList(LinkList *L,int length){
        LinkList temp,target;
        for (int i = 0; i < length; i ++) {
            if(*L == NULL){
                *L = (LinkList)malloc(sizeof(Node));
                if(*L == NULL){
                    return;
                }
                (*L)->data = i;
                (*L)->next = *L;
                
            }else{
                //要遍历查找到尾结点,然后在尾结点插入数据
                target = *L;
                while (target->next != *L) {
                    target = target->next;
                }
                temp = (LinkList)malloc(sizeof(Node));
                if(temp == NULL){
                    return;
                }
                temp->data = i;
                temp->next = *L;
                target->next = temp;
            }
        }
    }
    

    还有一种方式就是用一个指针变量始终保存着尾结点,创建的时候就只用尾指针来操作,从而省了每次的遍历查找

    void createLinkList2(LinkList *L,int length){
        LinkList temp,r;
        for (int i = 0; i < length; i ++) {
            if(*L == NULL){
                *L = (LinkList)malloc(sizeof(Node));
                if(*L == NULL){
                    return;
                }
                (*L)->data = i;
                (*L)->next = *L;
                r = *L;
            }else{
                temp = (LinkList)malloc(sizeof(Node));
                if(temp == NULL){
                    return;
                }
                temp->data = i;
                temp->next = *L;
                r->next = temp;
                r = temp;
            }
        }
    }
    

    单向循环链表的遍历,也是以首元结点来作为标志来判断的

    //遍历单向循环链表
    void traveceLinkList(LinkList L){
        LinkList temp = L;
        do {
            printf("%d\t",temp->data);
            temp = temp->next;
        } while (temp != L);
        printf("\n");
    }
    

    单向循环链表插入数据,其中插入数据有两种情况

    • 在首元结点插入数据,先将创建的结点指向首元结点,然后将尾结点指向创建的结点,再将指针L指向创建的结点(此时变为首元结点)
    • 不是在首元结点插入数据,需要找到需要插入的结点的前一个结点,然后创建的结点指向插入的结点,前一个结点指向创建的结点。
    void insertLinkList(LinkList *L,int place,int num){
        if(*L == NULL){
            return;
        }
        LinkList temp,target;
        if(place == 0){
            //找到尾结点
            target = *L;
            while (target->next != *L) {
                target = target->next;
            }
            
            temp = (LinkList)malloc(sizeof(Node));
            if(temp == NULL){
                return;
            }
            temp->data = num;
            temp->next = *L;
            target->next = temp;
            *L = temp;
        }else{
            target = *L;
            int j = 1;//用来计数的
            while (target->next != *L && j != place ) {
                target = target->next;
                j ++;
            }
            
            if(place > j){
                return;
            }
            
            temp = (LinkList)malloc(sizeof(Node));
            if(temp == NULL){
                return;
            }
            temp->data = num;
            temp->next = target->next;
            target->next = temp;
            
        }
    }
    

    单向循环链表的删除,与插入是差不多的逻辑,也是分两种情况的

    • 删除首元结点,如果只是本身的,直接释放掉,如果不是的需要查找到尾结点,然后将尾结点指向首元结点的下一个结点,还要将指针指向删除的首元结点的下一个结点,再做释放。
    • 删除的不是首元结点,需要找到需要删除的结点的前一个结点,然后将前一个结点指向需要删除结点的下一个结点,然后再做释放
    void deleteLinkList(LinkList *L,int place,ElemType *e){
        if(*L == NULL){
            return;
        }
        LinkList target,deleteItem;
        target = *L;
        //删除首元结点
        if(place == 0){
            if(target->next == *L){//本身
                deleteItem = *L;
                *e = deleteItem->data;
                *L = NULL;
            }else{
                //找到尾结点
                while (target->next != *L) {
                    target = target->next;
                }
                deleteItem = *L;
                target->next = deleteItem->next;
                *L = deleteItem->next;
                *e = deleteItem->data;
                free(deleteItem);
            }
        }else{
            int j = 1;
            while (target->next != *L && j != place) {
                target = target->next;
                j ++;
            }
            if(j < place){
                return;
            }
            deleteItem = target->next;
            target->next = deleteItem->next;
            *e = deleteItem->data;
            free(deleteItem);
            
        }
        
    }
    

    4 双向链表

    双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点后继结点,在双向链表尾结点的后继结点是为NULL的。在这部分内容中会设置一个头结点,其中这个头结点的前驱结点为NULL,数据域为-1,这头结点只是辅助功能。双链表的定义与上面的定义有所区别,多了一个前驱结点

    typedef struct Node{
        ElemType data;
        struct Node *next;
        struct Node *prior;
    } Node;
    
    typedef Node *LinkList;
    

    初始化双向链表,其中头结点在初始化的时候,前驱结点和后继结点都设置为NULL,数据域为-1,然后用后插法的方式给双链表赋值数据。

    //创建双向链表
    void createLinkList(LinkList *L){
        if(*L != NULL){
            return;
        }
        printf("创建双向链表\n");
        //初始化就有头结点
        *L = (LinkList)malloc(sizeof(Node));
        (*L)->prior = NULL;
        (*L)->next = NULL;
        (*L)->data = -1;
        
        LinkList temp;
        LinkList target = *L;
        
        for (int i = 0; i < 10; i ++) {
            temp = (LinkList)malloc(sizeof(Node));
            temp->data = i;
            temp->next = NULL;
            temp->prior = target;
            target->next = temp;
            target = temp;
        }
    }
    

    为了检验创建的双链表是否正确,打印遍历双链表

    //打印双向链表
    void showLinkList(LinkList L){
        if(L == NULL){
            return;
        }
        //头结点不打印出来的
        LinkList temp = L->next;
        while (temp != NULL) {
            printf("%d\t",temp->data);
            temp = temp->next;
        }
        printf("\n");
    }
    

    接下来就是双向链表的插入数据,传入插入的位置和数据,因为一开始就设置了有头结点,所以对首元结点的插入数据和其他位置的插入是没区别的,对需要找到插入数据的结点的前一个结点定义为target,然后对创建的结点定义为temp。首先需要对temp的next结点(需要插入数据的结点)的前驱结点改为temp,然后temp的next为target的next(需要插入数据的结点),然后target的next改为temp,temp的前驱结点(prior)为target结点,也要注意一下插入到最后一个结点。代码如下:

    void insertLinkList(LinkList *L,int place,ElemType e){
        if(*L == NULL || place < 1){
            return;
        }
        LinkList target = (*L);
        
        LinkList temp = (LinkList)malloc(sizeof(Node));
        temp->data = e;
        temp->prior = NULL;
        temp->next = NULL;
        
        //找到需要插入的位置的前一个结点
        int index = 1;
        while (target != NULL && index != place) {
            target = target->next;
            index ++;
        }
        if(target == NULL){
            return;
        }
        //这是在最后的结点插入数据
        if(target->next == NULL){
            target->next = temp;
            temp->prior = target;
        }else{
            target->next->prior = temp;
            temp->next = target->next;
            target->next = temp;
            temp->prior = target;
        }
    }
    
    

    双向链表的指定位置的删除,找到需要删除结点的上一个结点target,需要删除的结点为deleteItem,将target的next指向deleteItem的next,然后deleteItem的next的前驱结点指向target。


    image.png
    void deleteLinkList(LinkList *L,int place){
        if(*L == NULL || place < 1){
            return;
        }
        //找到需要删除的结点的前一个结点
        LinkList target = *L;
        int index = 1;
        while (target != NULL && index < place) {
            target = target->next;
            index ++;
        }
        
        if(target->next == NULL || index > place){
            return;
        }
        
        LinkList deleteItem = target->next;
        target->next = deleteItem->next;
        if(deleteItem->next != NULL){
            deleteItem->next->prior = target;
        }
        free(deleteItem);
        
    }
    

    5.双向循环链表

    双向循环链表的数据结构与双链表是一样的,区别就是:它的最后一个结点指向头结点,形成一个环。因此,从循环链表中的任何一个结点出发都能找到任何其他结点。

    image.png

    接下来就是创建一个带头结点的空的双向循环链表

    //创建一个带头结点的空的双向循环链表
    void initLinkList(LinkList *L){
        *L = (LinkList)malloc(sizeof(Node));
        (*L)->prior = (*L);
        (*L)->next = (*L);
        (*L)->data = -1;
    }
    

    对双向循环链表插入数据,要先找到插入数据结点的前驱结点,然后先给插入的结点temp分别设置前驱和后继结点这时还没打破链表的,再对前驱结点target的next指向temp,还要对如果temp的next不是尾结点的(没有指向头结点)前驱结点指向temp。


    image.png
    //双向循环链表插入数据
    void insertLinkList(LinkList *L,int place,ElemType e){
        if(*L == NULL){
            return;
        }
    
        int index = 0;
        LinkList target = *L;
        // 查找需要插入的位置
        while (index < place) {
            target = target->next;
            index ++;
        }
        if(index > place){
            return;
        }
    
        LinkList temp = (LinkList)malloc(sizeof(Node));
        if(temp == NULL){
            return;
        }
        temp->data = e;
        temp->prior = target;
        temp->next = target->next;
        target->next = temp;
        if(temp->next != *L){
            temp->next->prior = temp;
        }
    
    }
    

    双向循环链表的删除,通过位置找到需要删除的结点,因为有了头结点所以首元结点的删除与其他结点的删除是一样的。

    //双向循环链表的删除 e是删除的值
    void deleteLinkList(LinkList *L,int place,ElemType e){
        if((*L) == NULL || place < 1){
            return;
        }
        LinkList target = (*L);
        
        //如果删除到只剩下首元结点了,则直接将*L置空;
        if(target->next == *L){
            free(*L);
            (*L) = NULL;
            return;
        }
        
        int index = 0;
        //找到需要删除的位置的结点
        while (index < place && target->next != *L) {
            target = target->next;
            index ++;
        }
        //这是超过边界的
        if(place > index){
            return;
        }
        
        LinkList deleteTemp = target;
        target->prior->next = deleteTemp->next;
        target->next->prior = target->prior;
        free(deleteTemp);
    }
    

    最后

    线性表中的顺序表单链表单向循环链表双链表双向循环链表的操作就介绍完了。

    相关文章

      网友评论

          本文标题:数据结构与算法---线性表

          本文链接:https://www.haomeiwen.com/subject/sfbjhktx.html