08.链表与数据抽象

1.数据抽象

1.1 数据抽象的目的与意义

1.1.1 数据对象

信息缺失:程序中的数据对象只有地址和值,没有数据类型、数据解释及数据意义等信息

1.1.2 解决手段:抽象

数据的表示:注释、有意义的数据对象名称

数据的功能:描述可以在数据上工作的操作集

数据的功能比表示更重要

例:程序员更关心整数的运算而不是计算机如何存储整数

1.1.3 类型

细节由用户自定义,语言仅提供定义手段

1.1.4 成员

结构化数据类型的子数据对象

1.1.5 成员类型

每个成员具有确切的类型

1.1.6 成员数目

部分结构化数据类型可变,部分固定

1.1.7 成员组织

成员组织结构(线性结构或非线性结构)必须显式定义

1.1.8 操作集

可以在数据上进行的操作集合

1.2 结构化数据类型的性质

1.3 数据封装

1.3.1 数据封装定义

将数据结构的细节隐藏起来

1.3.2 实现方式

分别实现访问数据成员的存取函数

1.3.3 数据封装示例

struct DYNINTS{    //整型动态数组
    unsigned int capacity;  //数组中能存的最多元素个数
    unsigned int count;  //程序运行某时刻动态数组中元素个数
    int *items;   //指向动态数组,表示真实元素序列
    bool modified;  //数组是否发生改变标志位
};
unsigned int DiGetCount(DYNINTS* a)  //结构体的接口——数据封装的实质
{
    if(!a){
        cout<<"DiGetCount:Parameter illegal."<<endl;
        exit(1);
    }
    return a->count;
}

上述例子如果只定义结构体,而不提供下面的接口。缺陷在于,一旦改变结构体中的成员,所有使用结构体的相应代码都需随之改变。比如将count改为NUMBER_OF_ELEMENT。提供接口后,若改变count,只需重新设计结构体,而不会将变化传递至所有使用该结构体的代码

1.4 信息隐藏

1.4.1 数据封装的问题

只要将结构体类型定义在头文件中,库的使用者就可以看到该定义,并按照成员格式直接访问,而不是调用存取函数,即库的使用者有两种选择,可以自由选择使用存取函数还是采用成员格式直接访问

1.4.2 解决方法

将结构体类型的具体细节定义在源文件中,所有针对该类型量的操作都只能通过函数接口来进行,从而隐藏实现细节

1.4.3 信息隐藏示例

//头文件 "dynarray.h"
struct DYNINTS;
typedef struct DYNINTS *PDYNINTS;
//源文件  "dynarray.cpp"
struct DYNINTS{
    unsigned int capacity;
    unsigned int count;
    int *item;
    bool modified;
};

库的使用者只能看到头文件中结构体的声明,而看不到源文件中结构体的具体细节定义。

1.5 抽象数据类型

具体实例

设计能够存储二维平面上点的抽象数据类型

//点库接口 "point.h"
struct POINT;
typedef struct POINT *PPOINT;
PPOINT PtCreat(int x, int y);
void PtDestory(PPOINT point);
void PtGetValue(POINT point, int *x, int *y);
void PtSetValue(PPOINT point, int x, int y);
bool PtCompare(PPOINT point1, PPOINT point2);
char *PtTransformIntoString(PPOINT point);
void PtPrint(PPOINT point);
//点库实现 "point.cpp"
#include <cstdio>
#include <cstring>
#include <iostream>
#include "point.h"
using namespace std;
static char* DuplicateString(const char* s);
struct POINT{int x, y;};

PPOINT PtCreat(int x, int y)
{
    PPOINT t = new POINT; t->x = x; t->y = y; return t;
}
void PtDestory(PPOINT point)
{
    if(point){delete point;}
}
void PtGetValue(PPOINT point, int *x, int *y)
{
    if(point)
    {
        if(x)
            *x = point->x;
        if(y)
            *y = point->y;
    }
}
void PtSetValue(PPOINT point, int x, int y)
{
    if(point){point->x = x; point->y = y;}
}
bool PtCompare(PPOINT point1, PPOINT point2)
{
    if(!point1||!point2)
    {
        cout<<"PtCompare:Parameter(s) illegal."<<endl;
        exit(1);
    }
    return (point1->x == point2->x && point1->y == point2->y);
}
char *PtTransformIntoString(PPOINT point)
{
    char buf[BUFSIZ];
    if(point)
    {
        sprintf(buf,"(%d,%d)",point->x,point->y);
        return DuplicateString(buf);
    }
    else
        return NULL;
    
}
char* DuplicateString(const char* s)
{
    unsigned int n = strlen(s);
    char* t = new char[n+1];
    for(int i = 0;i < n; i++)
        t[i] = s[i];
    t[n] = '';
    return t;
}
void PtPrint(PPOINT point)
{
    if(point)
    {
        cout<<"("<<point->x<<","<<point->y<<")";
    }
    else
    {
        cout<<"NULL.";
    }    
}

2.链表

2.1 链表的意义与性质

存储顺序访问的数据对象集

数据对象占用的存储空间总是动态分配的

2.2 链表的定义

元素序列,每个元素与前后元素相链接

image-20200914130823758

结点:链表中的元素

表头、表尾:链表的头尾结点

头指针、尾指针:指向表头、表尾的指针

2.3 链表数据结构

2.3.1 链表结点:使用结构体类型表示

至少包含两个域:结点数据域与链接指针域

struct NODE;
typedef struct NODE *PNODE;
struct NODE{
    PPOINT data;    //当前结点的存储数据
    PNODE next;     //指向下一结点,表尾此域为NULL
};

2.3.2 链表结构:封装结点表示的细节

struct LIST;
typedef struct LIST *PLIST;
struct LIST{
    unsigned int count;   //链表中包含的节点数目
    PNODE head,tail;      //链表头尾指针
};

2.3.3 标准链表图例

image-20200914132010319

图上左边的count、head、tail是链表结构封装结点结构体定义的成员。每个链表结点结构体定义每个结点的数据域与指针域。

特别说明:结点总是动态分配内存的,所以结点逻辑上连续,物理上地址空间并不一定连续

时刻注意维护链表的完整性:一旦头指针head失去链表表头地址,整个链表就会丢失;任意结点next域失去下一结点地址,后续结点就会全部丢失

不同的指针指向可以构成不同的链表,如单向链表、双向链表、循环链表、双向循环链表

3.函数指针

3.1 函数指针的目的与意义:抽象数据与抽象代码

数据与算法的对立统一

函数的地址:函数入口位置,将该数值作为数据保存起来,就可以通过特殊手段调用该函数。

typedef void* ADT; typedef const void* CADT;

将链表所要存储的结点数据对象抽象成通用类型,不允许在链表库中出现与点数据结构相关的任何东西

3.2 函数指针的定义

3.2.1 函数指针的定义格式:

数据类型(*函数指针数据对象名称)(形式参数列表);

示例:char*(*as_string)(ADT object);

定义了一个函数指针类型的变量,变量名字是as_string,它是一个指针,将指向一个函数,这个函数带有一个ADT类型的参数,返回值类型为char*

3.2.2 函数指针的赋值

函数指针变量可以像普通变量一样赋值

同类型函数指针可以赋值,不同类型则不能赋值

如何确定函数指针类型是否相同:函数参数与返回值不完全相同

函数指针数据对象名称=函数名称;

char* DoTransformObjectIntoString(ADT object)
{return PtTransformIntoString((PPOINT)object);}
as_string = DoTransformObjectIntoString;  //将函数入口地址赋值给函数指针变量

3.3 函数指针的使用

通过函数指针调用函数

函数指针被赋值后,即指向实际函数的入口地址

通过函数指针可以直接调用它所指向的函数

调用示例:

char* return_value;
PPOINT pt= PtCreat(10,20);
as_string = DoTransformObjectIntoString;
return_value = as_string((ADT)pt);  //函数直接调用

要区分函数指针调用和函数直接调用,使用下述格式调用函数指针指向的函数:

returned_value = (*as_string)((ADT)pt);   /函数指针调用

3.4 函数指针类型

目的:用于区分不同类型的函数指针

typedef int(*COMPARE_OBJECT)(const void* e1,const void* e2);

前面添加typedef关键字,保证COMPARE_OBJECT为函数指针类型,而不是函数指针变量

可以像普通类型一样使用函数指针类型定义变量:

COMPARE_OBJECT compare = DoCompareObject;

4.抽象链表

实例:设计能够处理点数据类型的抽象链表库

链表的结点插入、结点删除是重点。

4.1 回调函数

允许调用函数指针调用未来才会实现的代码

回调函数:依赖后续设计才能确定的被调函数

示例:qsort()函数中的最后一个参数,数据比较函数的函数指针

4.2 回调函数参数

回调函数与主调函数的信息交互:附加信息

5.链表小结

5.1 链表的优点

插入和删除操作非常容易,不需要移动数据,只需要修改链表结点指针;与数组比较,数组插入和删除元素操作则需要移动数组元素,效率很低

5.2 链表的缺点

只能顺序访问,要访问某个结点,必须从前向后查找到该结点,不能直接访问

5.3 链表设计中存在的问题

链表要存储点数据结构,就必须了解点库的接口;如果要存储其他数据,就必须重新实现链表

原文地址:https://www.cnblogs.com/bear-Zhao/p/13669201.html