数据结构与算法(二):线性表

697 阅读9分钟

线性表

线性表是零个或多个数据元素的有限序列。

线性表的顺序存储结构:

线性表的顺序存储是指在内存中用地址连续的一块存储空间顺序存放线性表中的各数据元素,用这种存储形式存储的线性表称为顺序表。因为内存中的地址空间是线性的,所以用物理位置关系上的相邻性实现数据元素之间的逻辑相邻关系既简单又自然。

顺序表存储结构

顺序表的基本操作的实现

顺序表结构设计

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

1. 顺序表的初始化

Status InitList(Sqlist *L){
    //为顺序表分配一个大小为MAXSIZE 的数组空间
    L->data =  malloc(sizeof(ElemType) * MAXSIZE);
    //存储分配失败退出
    if(!L->data) exit(ERROR);
    //空表长度为0
    L->length = 0;
    return OK;
}

2. 顺序表的插入

/*
 初始条件:顺序线性表L已存在,1≤i≤ListLength(L);
 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
 */
Status ListInsert(Sqlist *L,int i,ElemType e){
    
    //i值不合法判断
    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];
        }
    }
    
    //将新元素e 放入第i个位置上
    L->data[i-1] = e;
    //长度+1;
    ++L->length;
    
    return OK;
    
}

3. 顺序表的取值

/*
 初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
 操作结果: 取出L的第i个数据元素,赋值给e
 */
Status GetElem(Sqlist L,int i, ElemType *e){
    //判断i值是否合理, 若不合理,返回ERROR
    if(i<1 || i > L.length) return  ERROR;
    //data[i-1]单元存储第i个数据元素.
    *e = L.data[i-1];
    
    return OK;
}

4. 顺序表删除

/*
 初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
 操作结果: 删除L的第i个数据元素,L的长度减1
 */
 Status ListDelete(Sqlist *L,int i){
    
    //线性表为空
    if(L->length == 0) return ERROR;
    
    //i值不合法判断
    if((i<1) || (i>L->length+1)) return ERROR;
    
    for(int j = i; j < L->length;j++){
        //被删除元素之后的元素向前移动
        L->data[j-1] = L->data[j];
    }
    //表长度-1;
    L->length --;
    
    return OK;
    
}

线性表的链式存储结构:

单链表

单链表是用一组任意的存储单元存放线性表的元素,这组存储单元可以连续也可以不连续,甚至可以零散分布在内存中的任意位置。为了能正确表示元素之间的逻辑关系,每个存储单元在存储数据元素的同时,还必须存储其后继元素所在的地址信息,这个地址信息称为指针(pointer),这两部分组成了数据元素的存储映像,称为结点(node)。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起的,由于每个结点只有一个指针域,故称为单链表。

结点结构

单链表的结构:

头节点、头指针和首元节点

  • 头指针:一个普通的指针,它的特点是永远指向链表第一个节点的位置。很明显,头指针用于指明链表的位置,便于后期找到链表并使用表中的数据;

    注意:链表中有头节点时,头指针指向头节点;反之,若链表中没有头节点,则头指针指向首元节点。

  • 节点:链表中的节点又细分为头节点、首元节点和其他节点:

  • 头节点:其实就是一个不存任何数据的空节点,通常作为链表的第一个节点。对于链表来说,头节点不是必须的,它的作用只是为了方便解决某些实际问题;

  • 首元节点:由于头节点(也就是空节点)的缘故,链表中称第一个存有数据的节点为首元节点。首元节点只是对链表中第一个存有数据节点的一个称谓,没有实际意义;

  • 其他节点:链表中其他的节点;

完整链表结构

单链表的基本操作

1.定义结点

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

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

typedef struct Node * LinkList;

2.初始化单链表

Status InitList(LinkList *L){
    
    //产生头结点,并使用L指向此头结点
    *L = (LinkList)malloc(sizeof(Node));
    //存储空间分配失败
    if(*L == NULL) return ERROR;
    //将头结点的指针域置空
    (*L)->next = NULL;
    
    return OK;
}

3.单链表插入

/*
 初始条件:单链表L已存在;
 操作结果:在L中第i个位置之后插入新的数据元素e;
 */
Status ListInsert(LinkList *L,int i,ElemType e){
 
    int j;
    LinkList p,s;
    p = *L;
    j = 1;
    
    //寻找第i-1个结点
    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;
    //将p的后继结点赋值给s的后继
    s->next = p->next;
    //将s赋值给p的后继
    p->next = s;
    
    return OK;
}

4.单链表取值

Status ListGetElem(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.单链表删除元素

Status ListDelete(LinkList *L,int i,ElemType *e){
    int j;
    LinkList p,q;
    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;
}

链表结构与顺序存储结构对⽐

存储分配⽅式

顺序存储结构⽤⽤⼀段连续的存储单元依次存储线性表的数据元素; 
单链表采⽤链式存储结构,⽤⼀组任意的存储单元存放线性表的元素;

时间性能

  • 查找:
顺序存储 O(1);
单链表O(n);
  • 插⼊和删除:
存储结构需要平均移动⼀个表⻓⼀半的元素,时间O(n); 
单链表查找某位置后的指针后,插⼊和删除为O(1);

空间性能

顺序存储结构需要预先分配存储空间,分太⼤,浪费空间;分⼩了,发⽣溢出; 
单链表不需要分配存储空间,只要有就可以分配, 元素个数也不受限制;

单向循环链表

将单链表的两头连接,使其成为了一个环状链表,就成为了单向循环链表。

循环链表实现约瑟夫环

约瑟夫环问题,是一个经典的循环链表问题,题意是:已知 n 个人(分别用编号 1,2,3,…,n 表示)围坐在一张圆桌周围,从编号为 k 的人开始顺时针报数,数到 m 的那个人出列;他的下一个人又从 1 开始,还是顺时针开始报数,数到 m 的那个人又出列;依次重复下去,直到圆桌上剩余一个人。 单向循环链表的基本操作和单向链表差别不大,所以我们来实现一个约瑟夫环。

#include <stdio.h>
#include "stdlib.h"

typedef struct Node{
    int number;//编号
    struct Node *next;
}Person;

//创建一个n人的链表 n>0
Person * initLinkList(int n){
    Person * head = (Person *)malloc(sizeof(Person));
    head->next = NULL;
    head->number = 1;
    Person * tail = head;
    for (int i= 2; i <= n;i++){
        Person *temp = (Person *)malloc(sizeof(Person));
        temp->number = i;
        temp->next = NULL;
        tail->next = temp;
        tail = temp;
    }
    //首尾相连
    tail->next = head;
  
    return head;
}
//从编号为 k 的人开始顺时针报数,数到 m 的那个人出列
void findAndKill(int k, int m, Person **head) {
    Person *tail = *head;
    //找到链表第一个结点的上一个结点,为删除操作做准备
    while (tail->next!=*head) {
        tail=tail->next;
    }
    //找到编号为k的人  tail要指向p的上一个人以便删除
    Person *p = *head;
    while (p->number != k) {
        tail = p;
        p = p->next;
    }
    //当p->next = p 时 说明链表中只有p结点了
    while (p->next != p) {
        //数m次
        for (int i = 1; i < m; i++) {
            tail = p;//记录p的上一个
            p = p->next;
        }
        //移除p
        tail->next = p->next;
        printf("出列的人编号为:%d\n", p->number);
        free(p);
        //p指向下一个人,继续数
        p = tail->next;
    }
    //head指向的人可能已经释放了 指向最后剩下的人
    *head = p;
    printf("最后剩下的人编号为:%d\n", p->number);
}

int main(int argc, const char * argv[]) {
    
    Person *head =  initLinkList(15);
    printf("打印head :%d\n", head->number);
    findAndKill(8, 1, &head);
    printf("打印head :%d\n", head->number);
    return 0;
}

输出结果

打印head :1
出列的人编号为:8
出列的人编号为:9
出列的人编号为:10
出列的人编号为:11
出列的人编号为:12
出列的人编号为:13
出列的人编号为:14
出列的人编号为:15
出列的人编号为:1
出列的人编号为:2
出列的人编号为:3
出列的人编号为:4
出列的人编号为:5
出列的人编号为:6
最后剩下的人编号为:7
打印head :7

双向链表

如果希望快速确定单链表中任一结点的前驱结点,可以在单链表的每个结点中再设置一个指向其前驱结点的指针域,这样就形成了双向链表。

双向链表的结构

  • 双向链表的结点构成:

  • 双向链表结构

双向链表的基本操作

双向循环链表

和单向循环链表一样,将双向链表的尾结点部与头结点相连接,就成了双向循环链表。