【学习记录】线性表 20200330

303 阅读7分钟

线性表

线性表是最基本、最简单、也是最常用的一种数据结构。

特点:

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

本文所涉及的代码在这,参看代码,可以帮助你的理解。

线性表的顺序存储

我们可以使用顺序存储的方式来实现线性表。

代码定义

我们对 数据结构、状态、类型和一些常数进行定义。


//线性表的最大容量
#define MAXSIZE 100
//操作状态-成功
#define SUCCESS 1
//操作状态-失败
#define ERROR 0

#define TRUE 1
#define FALSE 0

//表内元素的类型 假设为 int
typedef int ItemType;
//返回各个函数的操作码
typedef int Status;

//顺序表
typedef struct {
//    指向一块连续的内存空间来存储数据
    ItemType *data;
//    表的长度
    int length;
}List;

初始化

为线性表开辟一段连续的内存空间。


Status initList(List *l){
//    开辟一段连续的空间给顺序表
    l->data = malloc(sizeof(ItemType) * MAXSIZE);
//    如果存储分配失败,则退出
    if (!l->data) exit(ERROR);
    
    l->length = 0;
    return SUCCESS;
        
}

输出

访问并输出线性表的各个元素。线性表的元素访问可以直接通过下标来访问。

//遍历并打印线性表
Status listPrint(List l){
    if (!l.length) {
        printf("Empty list.");
        return ERROR;
    }
    printf("数组输出:");
    for (int i = 0; i < l.length; i++) {
        printf("%d ",l.data[i]);
    }
    printf("\n");
    return SUCCESS;
}

为特定位置添加新的元素。注意需要对线性表的length进行操作,还要进行一定的容错判断。


/// 在i位置插入新的元素 e
/// @param l 数组
/// @param i 位置
/// @param e 新元素
Status listInsert(List *l, int i, ItemType 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--) {
//            将插入位置以及之后的位置向后移动一位
            l->data[j+1] = l->data[j];
        }
    }
//    将新元素放入i位置
    l->data[i-1] = e;
//    长度+1
    l->length++;
    return SUCCESS;
}

删除特定位置的元素。注意需要对线性表的length进行操作,还要进行一定的容错判断,删除元素需要带回


/// 删除 i 位置的原素
/// @param l 数组
/// @param i 位置
/// @param e 删除元素带回
Status listDelete(List *l, int i, ItemType *e){
    if (i < 1 || i > l->length ) return ERROR;
//    保留原值
    *e = l->data[i-1];
    for (int j = i; j < l->length; j++) {
//        被删除元素之后的元素向前移动一位
        l->data[j-1] = l->data[j];
    }
//    表长度减一
    l->length--;
    return SUCCESS;
}


/// 获取某值
/// @param l 列表
/// @param i 位置
/// @param e 获取元素带回
Status listGet(List l, int i, ItemType *e){
    if (i < 1 || i > l.length + 1) return ERROR;
    *e = l.data[i-1];
    return SUCCESS;
}

建设中...

清空

//1.5 清空顺序表
/* 初始条件:顺序线性表L已存在。操作结果:将L重置为空表 */
Status ClearList(Sqlist *L)
{
    L->length=0;
    return OK;
}

销毁

建设中...

线性表的链式存储

线性表在链式存储时需要额外的指针域来记录结点间的关系。

代码定义

我们对 数据结构、状态、类型和一些常数进行定义。


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

#define SUCCESS 1
#define ERROR 0

#define TRUE 1
#define FALSE 0

//表内元素的类型 假设为 int
typedef int ItemType;
//返回各个函数的操作码
typedef int Status;

//链式表的节点定义
typedef struct Node{
//    数据域
    ItemType data;
//    指针域
    struct Node *next;
}Node;

typedef struct Node* List;

初始化

链表的初始化需要注意的是我们常常在首元结点前添加头结点[1]。这样可以便于操作首元结点,也便于空表和非空表的统一操作。

Status initList(List *l){
//    生成头结点
    *l = (List)malloc(sizeof(Node));
//    如果存储分配失败,则退出
    if (*l == NULL) exit(ERROR);
//    头结点的指针域置空
    (*l)->next = NULL;
    return SUCCESS;
        
}

当然我们也可以使用头插法尾插法来初始化链表。

  • 头插法
//3.1 单链表前插入法(头插法)
/* 随机产生n个元素值,建立带表头结点的单链线性表L(前插法)*/
void createListHead(List *l, int n){
    
    List p;
    
    //建立1个带头结点的单链表
    *l = (List)malloc(sizeof(Node));
    (*l)->next = NULL;
    
    //循环前插入随机数据
    for(int i = 0; i < n;i++)
    {
        //生成新结点
        p = (List)malloc(sizeof(Node));
       
        //i赋值给新结点的data
        p->data = i;
        //p->next = 头结点的L->next
        p->next = (*l)->next;
        
        //将结点P插入到头结点之后;
        (*l)->next = p;
        
    }
}
  • 尾插法
    //3.2 单链表后插入法(尾插法)
/* 随机产生n个元素值,建立带表头结点的单链线性表L(后插法)*/
void createListTail(List *L, int n){
    
    List p,r;
 
    //建立1个带头结点的单链表
    *L = (List)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;
    
}

输出

访问并输出链表的各个元素。链表的元素访问需要逐个访问每个元素的指针域才能找到下一个元素。


//遍历并打印线性表
Status listPrint(List l){
    
    List p = l->next;
    printf("链表输出:");
    while (p) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
    return SUCCESS;
}

在链表的特定位置增加新的结点。注意容错。

/// 在i位置插入新的元素 e
/// @param l 链表
/// @param i 位置
/// @param e 新元素
Status listInsert(List *l, int i, ItemType e){
    int j;
    List p, s;
    p = *l;
    j = 1;
//    找到第 i-1 个结点
    while (p && j < i) {
        p = p->next;
        j++;
    }
    
    if(!p || j > i) return ERROR;
    
    s = (List)malloc(sizeof(Node));
    s->data = e;
    s->next = p->next;
    p->next = s;
    return SUCCESS;
}

删除链表的特定位置的结点。注意容错。

/// 删除 i 位置的原素
/// @param l 链表
/// @param i 位置
/// @param e 删除元素带回
Status listDelete(List *l, int i, ItemType *e){
    int j;
    List p,q;
    p = (*l);
    j = 1;
    
    //查找第i-1个结点,p指向该结点
    while (p->next && j < i) {
        p = p->next;
        ++j;
    }
    
    //当i>n 或者 i<1 时,删除位置不合理
    if (!(p->next) || (j>i)) return  ERROR;
    
    //q指向要删除的结点
    q = p->next;
    //将q的后继赋值给p的后继
    p->next = q->next;
    //将q结点中的数据给e
    *e = q->data;
    //让系统回收此结点,释放内存;
    free(q);
    
    return SUCCESS;
    
}

获取特定位置结点的值。

/// 获取某值
/// @param l 列表
/// @param i 位置
/// @param e 获取元素带回
Status listGet(List l, int i, ItemType *e){
    int j;
    List p;
    p = l->next;
    j = 1;
    
//    找到第 i 个
    while (p && j < i) {
        p = p->next;
        j++;
    }
    
//    没找到
    if (!p || j > i) return ERROR;
    
    *e = p->data;
    
    return SUCCESS;
}

建设中...

清空

建设中...

销毁

建设中...


  1. 头结点和哨兵:一般地,我们可以空出表的第一个元素用作“哨兵”。(本示例未使用哨兵)

    在许多算法中,存在“相邻依赖问题”(我自己造的词),在处理当前元素时,要涉及到它旁边那个元素。那如果当前元素是边界元素呢,它没有旁边那个元素,如果不作处理,程序就可能出错;如果对它特别对待,就会增加代码复杂性,还会降低程序效率。应用哨兵,也就是申请若干个多余的元素作为边界元素的邻居,可以完美得解决这个问题。

    同时,哨兵元素可以用来存储一些状态值,或者用作临时变量。 ↩︎