C初学者如何从内置基本数据类型进阶到抽象高级数据类型

来源:互联网 发布:查看淘宝店铺数据 编辑:程序博客网 时间:2024/05/17 01:49

注:《C primer plus》笔记

内置数据类型:
一、基本类型:
    有符号整数:int、long(long int)、short(short int)、long long(long long int)
    无符号整数:unsigned int;unsigned long;unsigned short
    字符:char
    布尔值:_Bool
    实浮点数:float、double、long double
    复数和虚浮点数:float _Complex、double _Complex、long double _Complex、float _Imaginary、double _Imaginary、long double _Imaginary
    
二、衍生类型:
    数组、字符串、指针、结构、联合

三、抽象数据类型:
    链表、二叉搜索树

问题1:如何设计一个存储电影信息的程序,其中电影的数量是可变。电影信息包含电影片名,电影票价,电影评分(0-10)。
分析:从其成员来看,包含的数据类型包含字符串数组、float和unsigned int;从其数量来看,用结构的数组来存储所有电影信息;从其数量的可变性质来看,结构数组的大小最好通过动态分配。综上所述,使用malloc()函数为一个结构指针动态分配内存,然后用该指针当做数组来对结构进行存储。

问题2:要求对问题1的电影数量不提前确定,即既非编译前确定数量,也非运行程序的开始确定数量,而是想用多少数量,就用多少数量,直到内存分配完(或达到操作系统给一个进程的内存分配限制或者操作系统的内存溢出保护限制)。
分析:这种情况最笨的方法是直接分配最大的一块内存空间,任由程序使用。灵活的方法是每次添加电影信息的时候,才分配内存。但是这种多次分配内存的方法,就可能会产生不连续或顺序错乱的内存分配,也就是说,使用数组存储数据,就不可能了。
如何处理这种多次分配内存产生的数据的管理呢?可以创建一个足够大的指针数组来依次存储这些多次分配的内存的地址,但是这种情况,仍然会在数组没用尽的时候浪费内存空间,并且具有这个“足够大”的上限。另一个方法就是重新定义电影信息这个结构,使其包含指向下一个结构的指针。
    struct film {
        char title[40];
        float price;
        int rating;
        struct film * next; //注意,结构本身不能含有同类型的结构,但是可以含有指向同类型结构的指针,这是链表的基础
    };
这个结构如何工作?我们来假设场景:输入第一部电影信息,程序需要设计不检查第一步电影信息的上个电影信息,因为它是第一部,然后设置next指针的值为NULL,因为它本身也是最后一部。第一个结构的指针称为头指针,指向我们需要存储的电影信息的第一个地址,这个地址我们需要保存到一个变量中,以备使用。然后输入第二部电影信息,程序分配内存,存储电影本身的信息后,将next指针设置为NULL,到这里,程序执行的操作和存储第一步电影信息时差不多,下面进入不同的部分,接下来,程序通过头指针,循环查找next,直至next为NULL,这个操作就是找到存储电影信息的这串信息中的最后一条(或者每次添加之后保存最后一条的地址,以备使用),然后将存储第二部电影信息的结构的地址,存入到最后一个结构的next指针中,这样就完成了对接。
    while(ptr)  
        ptr = ptr->next;
    ptr = second;
在本场景中,就是找到头指针的next,并将第二部电影信息的结构地址存储其中。当需要存储第三部电影信息时,重复存储第二部电影信息的操作,创建第三部电影信息的结构,并将地址存储到从头指针开始查找到最后一个结构的next指针中。一直循环操作下去,直至退出程序或者分配内存失败。
这种结构中包含下一个结构地址的抽象高级数据类型,叫做链表。形象地描述,就像一辆多节的火车。
下面用程序实现用链表存储电影信息
#include <stdio.h>
#include <stdlib.h>     //提供malloc()原型
#include <string.h>    //提供strcpy()原型
#define TSIZE 45
struct film {
    char title[TSIZE];
    int rating;
    struct film * next;    //指向链表的下一个结构
};
int main(void)
{
    struct film * head = NULL;//初始化头指针为空。
    struct film * prev,* current,* next;
    char input[TSIZE];
  /*收集并存储信息*/
    puts("Enter first movie title:");
    while(gets(input) != NULL && input[0] != '\0')
    {
        current = (struct film *)malloc(sizeof(struct film));
        if(head == NULL)  //第一个结构
            head = current;
        else                       //后续结构
            prev->next = current;   //不是第一个结构,那么就把当前结构的地址,存入到上一个结构的next指针中,进行对接。
        current->next = NULL;     //作为最后一个项目,下一个项目当然是空了
        strcpy(current->title,input);
        puts("Enter your rating <0-10>:");
        scanf("%d",&current->rating);
        while(getchar() != '\n'); //除去回车等多余输入
        puts("Enter next movie title (empty line to stop):");
        prev = current;    //当前的结构地址存入prev,以便添加下一个项目时,可以方便地找到上一个结构的next指针,并进行赋值。
    }
    //显示电影列表
    if(head == NULL)
        printf("No data entered.");
    else
        printf("Here is the movie list:\n");
    current = head;
    while(current != NULL)
    {
        printf("Movie:%s Rating:%d\n",current->title,current->rating);
        current = current->next;
    }
//释放所有分配的内存
    current = head;
    while(current != NULL)
    {
        next = current->next;  //指针保存下一个结构的地址
        free(current);                //释放当前指针指向的结构的内存
        current = next;             //将下一个结构的地址赋值给current以便循环操作。
//     free(current);   这两句是书上的源代码,替代上面的三句,但是我认为这样做有隐患,因为current指向的地址内存已经释放了,那么该地址中的数据就是无效数据,包括内存上存储的下一个结构的地址,那么在下一句中将其地址赋值给指针current,将会导致current存储的地址有可能不是原current->next存储的地址,就会导致错误。
//     current = current->next;    
    }
    printf("Bye!\n");

    return 0;
}

本示例程序中,没有对编码细节和概念模型进行分割(比如,用函数来实现显示、分配和释放的功能),并隐藏分配内存等操作细节,这样对小程序是没有太大影响的,但是对于代码稍多的程序,甚至是工程来讲,就会影响程序设计,这时候,就需要对问题进行抽象,对细节进行隐藏,只有这样才能实现更复杂的功能。

抽象数据类型
类型由属性集和操作集组成。例如int的属性是整数,表示的是数值。可对int执行的操作包括整数的几乎所有操作,如+-*/%等。
那么,创建一个新的类型,就需要三个步骤来实现从抽象到具体的过程:
1,为类型的属性和可对类型执行的操作提供一个抽象的描述。这个描述不应受任何特定实现的约束,甚至不应受到任何特定编程语言的约束。这样一种正式的抽象描述被称为抽象数据类型(ADT)。
2,开发一个实现该ADT的编程接口。即说明如何存储数据并描述用于执行所需操作的函数集合。比如在C中,您可能同时提供一个结构的定义和用来操作该结构的函数的原型。这些函数对用户自定义类型的作用和C的内置运算符对C基本类型的作用一样。想要使用这种新类型的人可以使用这个接口来进行编程。
3,编写代码来实现这个接口。当然,这一步至关重要,但是使用这种新类型的程序员无需了解实现的细节。

问题3:如何使用抽象数据类型来创建一个链表类型?如何对电影信息进行存储。
分析:按照抽象数据类型的具体实现的步奏进行。
1,抽象描述
类型名称
简单列表
类型属性:
可以保存一个项目序列
类型操作:
把列表初始化为空列表

确定列表是否为空

确定列表是否已满

确定列表中项目的个数

向列表末尾添加项目

遍历列表,处理列表中的每个项目

清空列表

2,构造接口
要求完成的接口可以隐藏编程细节,并且包含所有的类型操作,并按照抽象的过程,先声明类型操作的函数的原型,再利用接口实现整个程序,最后实现这些函数的功能。所以先构造list.h
,第二步编写films3.c,最后实现list.c。
//list.h
#ifndef LIST_H_    //和末尾的#endif形成代码块区域,意思是如果之前未包含本文件,则执行下面的声明语句,以免重复声明
#define LIST_H_   //如果未包含,那就声明为包含,因为正在执行声明
#include <stdbool.h>
//构造项目
#define TSIZE 45
struct film {
    char title[TSIZE];
    int rating;
};
typedef struct film Item;
//构造包含项目和下一个项目地址的节点
typedef struct node {
    Item item;
    struct node * next;
} Node;
//构造列表,列表是指向Node的指针。
typedef Node * List;
//初始化列表,InitializeList(&plist);为何需要使用地址符&?因为要修改该指针中的值,所以只能传地址而非变量作为参数。
//操作前,plist指向一个列表,操作后,该列表被初始化为空列表
void InitializeList(List * plist);
//确认列表是否为空列表  ListIsEmpty(plist);
//如果为空,返回true,否则返回false
bool ListIsEmpty(const List * plist);
//确认列表是否为满列表  ListIsFull(plist);
//如果为满,返回true,否则返回false
bool ListIsFull(const List * plist);
//计算列表中项目(节点)的个数ListItemCount(plist);
//返回项目的个数
unsigned int ListItemCount(const List * plist);
//在列表尾部添加一个项目AddItem(item,&plist);
//添加成功返回true,否则返回false
bool AddItem(Item item,List * plist);
//把一个函数作用于列表中的每个项目Traverse(plist,pfun);
void Traverse(const List * plist,void (* pfun)(Item item));
//清空为项目分配的内存 EmptyTheList(plist);
//列表变成空列表
void EmptyTheList(List * plist);
//结束声明
#endif

3,使用接口
//film3.c
#include <stdio.h>
#include <stdlib.h>
#include "list.h"
void showmovies(Item item);

int main(void)
{
    List movies;
    Item temp;

    InitializeList(&movies);
    if(ListIsFull(&movies))
    {
        fprintf(stderr,"No memeory available!Bye!\n");
        exit(1);
    }

    puts("Enter first movie title:");
    while(gets(temp.title) != NULL && temp.title[0] != '\0')
    {
        puts("Enter your rating <0-10>:");
        scanf("%d",&temp.rating);
        while(getchar() != '\n');
        if(AddItem(temp,&movies) == false)
        {
            fprintf(stderr,"Problem allocating memory\n");
            break;
        }
        if(ListIsFull(&movies))
        {
            puts("The list is now full.");
            break;
        }
        puts("Enter next movie title(empty line to stop):");
    }
    
    if(ListIsEmpty(&movies))
        printf("No data entered.");
    else
    {
        printf("Here is the movie list :\n");
        Traverse(&movies,showmovies);
    }
    printf("You entered %d movies.\n",ListItemCount(&movies));
    
    EmptyTheList(&movies);
    printf("Bye!\n");
    return 0;
}
void showmovies(Item item)
{
    printf("Movies:%s Rating:%d\n",item.title,item.rating);
}
注意:原书上除了InitializeList(),AddItem(),EmptyTheList(),其余函数使用参数movies时都未添加地址符&,这是错误的,编译时会警告并且运行不能达到预期效果,解决办法是所有接口函数使用movies时都带上&,或者修改list.h和下面的list.c中的函数定义,将非InitializeList(),AddItem(),EmptyTheList()函数的其中的List *的*去掉,并修改其代码块中的变量使用也去掉*。
4,实现接口
//list.c
#include <stdio.h>
#include <stdlib.h>
#include "list.h"

static void CopyToNode(Item item,Node * pnode);

void InitializeList(List * plist)
{
    * plist = NULL;
}

bool ListIsEmpty(const List * plist)
{
    if(* plist == NULL)
        return true;
    else
        return false;
}

bool ListIsFull(const List * plist)
{
    Node * pt;
    bool full;

    pt = (Node *)malloc(sizeof(Node));
    if(pt == NULL)
        full = true;
    else
        full = false;
    return full;
}

unsigned int ListItemCount(const List * plist)
{
    unsigned int count = 0;
    Node * pnode = * plist;

    while(pnode != NULL)
    {
        ++count;
        pnode = pnode->next;
    }
    return count;
}

bool AddItem(Item item,List * plist)
{
    Node * pnew;
    Node * scan = * plist;

    pnew = (Node *)malloc(sizeof(Node));
    if(pnew == NULL)
        return false;
    
    CopyToNode(item,pnew);
    pnew->next = NULL;
    if(scan == NULL)
        * plist = pnew;
    else
    {
        while(scan->next != NULL)
            scan = scan->next;
        scan->next = pnew;
    }
    return true;
}

void Traverse(const List * plist,void (* pfun)(Item item))
{
    Node * pnode = *plist;
    while(pnode != NULL)
    {
        (* pfun)(pnode->item);
        pnode = pnode->next;
    }
}

void EmptyTheList(List * plist)
{
    Node * psave;
    while(*plist != NULL)
    {
        psave = (*plist)->next;
        free(*plist);
        *plist = psave;
    }
}

static void CopyToNode(Item item,Node * pnode)
{
    pnode->item = item;
}

5,编译运行
list.h,list.c和films3.c都存放于同一目录,执行
#gcc list.c films3.c
#./a.out
Enter first movie title:
a
Enter your rating <0-10>:
10
Enter next movie title(empty line to stop):
b
Enter your rating <0-10>:
9
Enter next movie title(empty line to stop):
c
Enter your rating <0-10>:
8
Enter next movie title(empty line to stop):
d
Enter your rating <0-10>:
7
Enter next movie title(empty line to stop):
Here is the movie list :
Movies:a Rating:10
Movies:b Rating:9
Movies:c Rating:8
Movies:d Rating:7
You entered 4 movies.
Bye!

问题4:如果我需要对链表中间的项目进行插入、删除或替换呢?
分析:不难实现,通过计数定位(第N个)、查找定位(电影名是Tatnic),然后保存上一个项目的地址和当前项目的地址,如果插入,则将上一个项目的next指向新增的一个项目,然后新增的项目的next指向当前的项目;如果删除,则将当前项目的next保存到上一个项目的next中,然后释放当前项目的内存;如果替换,则直接替换当前项目的Item即可。同理,这些操作适用于链表头部和尾部。
特殊的链表:
只能尾部加入,头部删除的链表叫做——队列。
只能尾部加入,尾部删除的链表叫做——栈。

问题5:链表相比数组有一些优势,那有没有缺点呢?能否总结一下链表和数组的优缺点?
分析:以表格形式展现
数据形式
优点
缺点
数组
C对其直接支持
提供随机访问
编译时决定其大小,插入和删除元素很费时
链表
运行时决定其大小,快速插入和删除元素
不能随机访问,用户必须提供编程支持
可为随机访问?就是直接访问数据结构中某个项目的操作。与之对应的是顺序访问,就是按照数据结构的起始位置一直访问到末尾。
链表要访问某个项目,必须根据提供的位置或匹配项从头开始查找,而数组则可以根据其索引直接访问,如果有大量的这种随机访问操作,使用链表将会成为性能瓶颈。

为什么数组插入和删除元素很费时?因为数组是连续的内存空间,如果删除一个项目,那么就必须将后面的项目依次前移,填补空白,如果插入一个项目,需要将当前位置之后的项目全部后移一位,才能将项目存入当前的内存,如果删除时不移动项目,那么多次删除之后,数组的可用项目位置将会大大减少,如果插入时不移动项目,插入动作将会变成替换。这样插入和删除一个项目,都要大量移动其他项目的操作,将会非常耗时。而链表为何又可以快速插入删除呢?这取决于链表的内存分散性,只需将新增的项目链接到链表中和将删除的项目之后的项目与其之前的项目连接起来即可。

问题6:那有没有一种数据结构,包含数组和链表的优点,又避免它们的缺点呢?
分析:有,那就是二叉搜索树。
考虑一种搜索方法,将所有项目按照顺序排列,当需要查找某个项目,则从项目的中间开始查找,如果等于,则返回结果,如果比中间的项目大,则到所有项目的后半部分查找;如果比中间的项目小,则到所有项目的前半部分查找。再查查找时,仍然到该部分的中间进行查找,依次类推,最后找到,则返回结果,没找到,则报告查找失败。
这种搜索方法,比数组的按照索引查找要慢一点,因为需要若干次的查找,但是比起顺序查找,却是要快上许多,这种查找叫做二分查找或者折半查找。研究表明:二分查找查找到一个项目需要的最多次数为n,那么它查找的数据总量则为2 ** n - 1(2的n次方减去1),数据总量越大,优势越明显。
二分查找和链表正是二叉搜索树的基础。
二叉搜索树的抽象模型如下:

该模型就像一颗倒置的树,最上层是根,左边是左子节点,其下又有节点,因此形成左子树。右边是右子节点,其下又有节点,因此形成右子树。没有子节点的节点,称为叶节点。
其中的left和right是存储子节点的地址的指针变量,通过这种两个分支的做法,可以容易实现二分搜索。
通过二叉搜索树,就可以实现随机快速访问,快速插入和删除以及运行时决定其大小的优点。

问题7:二叉搜索树有缺点吗?
分析:有,首先,实现二叉树的接口比链表复杂,详细见书本,其次,二叉搜索树的优点是基于所有的项目呈平衡状态的情况下,何谓平衡状态?也就是根节点的左右子树的节点数量和节点深度都差不多,最好是每个节点都有两个子节点直至叶节点的情况。至于如何让数变得平衡,这又是另一个更深的话题了。













0 0
原创粉丝点击