指针的指针应用||数据结构

一、概述

本篇文章主要通过单项链表的有序排序、链式搜索二叉树来探索C的能力即“C能够取得现存对象的地址”,与指针的指针通过间接操作符“*”改变指针所指向的指针的能力如出一辙。因此在读本篇文章之前需要基础的指针、链表、二叉树知识,当然接下来也会简单介绍指针的指针。

指针作为C这门高级语言的重要组成部分,因其操作内存的高风险与高效率引得众人心向往之,为此该篇文章着重突出“改变地址指向的内容”以解决复杂的数据结构问题。

二、指针的指针

首先我们声明一个整型变量:

int a=10;

对于以下表达式我们是否能清晰的明白含义:

*&a=20;

而a最终的结果是10还是20?

以上的表达式首先看左值*&a的含义,根据优先级先对a取地址,在通过间接操作符指向地址的内容,即左值是变量a本身,最后将右值赋值给左值a,结果与以下表达式一样:

a=20;

因此a最终结果为20。

倘若我们再声明一个指针变量指向整型变量a:

int a=10;

int* b;

b=&a;

或者再次声明一个变量:

int**p;

p=&b;

那么*b=20或者**p=20都可以达到a=20的目的。

由于举例颇为简单,只是介绍指针的指针的用法,并不能感受“C能够取得现存对象的地址”这个强大能力的魅力,为此接下来从复杂问题里探索这个能力的奥秘。

三、单项链表


图3.1

内存中存在着多个相同的结构类型且相关联的数据,构成一个集合。把每个独立的结构数据称之为节点,每个节点又包含一个指向下一个节点的指针,以及其他必要的数据。如图1所示,我们声明一个结构类型如下:

typedef struct  TABLENODE {

int value;

struct  TABLENODE* next;

}TableNode;

其中,整形变量value是节点的内容,next指向下一个节点。

如图1所示,当查找value值为11的节点,需要通过根节点找到第一个节点,再根据第一个节点的next指针寻找到第二个节点,依次并寻找到11这个节点。要说明的是根节点是指向第一个节点,并不不包含其他数据。另外我们图上有序的逻辑排列方便于直观探讨,而在实际物理内存中,不同节点占有的内存并不是连续的。

简单介绍完单项链表的含义,于是,可以引出如下思考:

如何将图1中的value值为9的节点删除,且不影响链表的完整性?

分析:

根据单项链表的不可逆性,从根节点依次查找节点的顺序是唯一的,通过比较当前节点的值与9的大小,如果不相等,则继续查找下一个节点,这也是循环判断的条件。

重要的是当查找到值为9这个当前节点时,接下来要进行的操作是提取当前节点指向的下一个节点11,并且要满足当前节点的前一个节点7的next指针指向11这个节点,最后删除9这个节点。

因此需要保存当前指针的前一个节点的指针。

步骤如下:

图3.2

第一步:如图3.2,查找第一个节点时,当前指针current指向第一个value值为3的节点,即current->value=3,与我们要删除的9是不等的,因此在current指针继续指向下一个节点之前,将current保存到previous。

图3.,3

第二步:如图3.3,查找第二个节点时,当前指针current指向第2个value值为7的节点,即current->value=7,与我们要删除的9是不等的,因此在current指针继续指向下一个节点之前,将current保存到previous。

图3.4

第三步:如图3.4,查找第3个节点时,当前指针current指向第3个value值为9的节点,即current->value=9,发现与我们要删除的9是相等的,此时只需要将previous节点的next指针指向current节点指向的下一个节点。得到图3.5所示。

图3.5

第一次尝试Delete函数如下:

void Delete(TableNode* node,int value)

{

    TableNode*current;

    TableNode*previous;

    previous=NULL;

    current= node;

    while((current!=NULL)&&(current->value!=value))

    {

        previous=current;

        current=current->Next;

    }

    previous->Next=current->Next;

    free(current);//销毁内存

}

第一次尝试删除函数存在如下问题:

1.传递的形参为根节点,如果需要删除第一个节点,就需要改变根节点的指向,由于形参传递不能改变形参本身的值,所以以上代码未考虑到这种情况。针对这种情况,我们只需要根据“函数的形参为指针时,可以改变指针指向的内容”这一特点,传递参数由根指针转换为根指针的地址。

2.传递的根节点有可能是空的,会导致其他问题。

对以上代码做如下改进:

void Delete(TableNode** node,int value)

{

    assert(node!=NULL);

    TableNode*current;

    TableNode*previous;

    previous=NULL;

    current= *node;

    while((current!=NULL)&&(current->value!=value))

    {

        previous=current;

        current=current->Next;

    }

    assert(current!=NULL);

    if(previous==NULL)

    {

        *node=current->Next;

    }

    else

    {

        previous->Next=current->Next;

    }

        free(current);

}

第二次尝试是可以达到正常解决问题的目的,但这并不是这篇文章的主旨,也未突出“指针的指针通过间接操作符*改变指针指向的指针”这一思想。

我们将根指针的地址即二维指针作为形参传递到函数,用一个一维指针current指向当前节点。当第一个节点不是删除对象时,我们只需使用二维指针保留current->next的地址,这个时候二维指针的意义变为指向当前节点的next指针。作为循环条件,在下次循环之前,重新将二维指针指向current,这样当前节点current指针自动前往下一个节,作为循环的条件进行判断。

因此我们只需要“指向当前节点的指针”以及“指向当前节点的next指针的指针”。

步骤如下:

图3.6

第一步:如图3.6,p为根节点的地址,此时*p指向第一个value为3的节点,与current等价。

图3.7

第二步:如图3.7,可以注意到p指向的是第一个节点的next指针位置,而不是第一个节点,这个至关重要。此时在下次循环之前p指向的内容保留到current指针,自动使得current指向value为7的节点。

图3.8

第三步:如图3.8,p指向的是第二个节点的next指针位置。此时在下次循环之前p指向的内容保留到current指针,自动使得current指向value为9的节点。

图3.9

第四步:如图3.9,判断当前的节点是需要删除的节点。将p指向当前节点current的next指针。

代码如下:

void Delete(TableNode** node,int value)

{

    assert(node!=NULL);

    TableNode*current;

    TableNode**p=node;

    current= *p;

    while((current!=NULL)&&(current->value!=value))

    {

        p=¤t->Next;

        current=*p;

    }

    assert(current!=NULL);

    *p=current->next;

}

第三次尝试可以看到,并没有采取第二次尝试中的保留前一个节点的方式,而是抓住“指针的指针通过间接操作符*改变指针指向的指针”这一思想解决问题。而这类思想在链式搜索二叉树中也有所使用。

事情并没有结束,我们很快知道插入一个节点也可以这么操作,可以参照删除的思想,只贴上代码:

void insert(TableNode** node,int value)

{

    assert(node!=NULL);

    TableNode*current;

    TableNode**p=node;

    TableNode*insertnode;

    current= *p;

    while((current!=NULL)&&(current->value

    {

        p=¤t->Next;

        current=*p;

    }

    //新增:

    insertnode=(TableNode*)malloc(sizeof(TableNode));

    assert(insertnode!=NULL);

    insertnode->value=value;

    insertnode->next=current;

    *p=insertnode;

}

四、链式搜索二叉树

图4.1

二叉树的特点:当前节点的值要大于它左子树所有的节点值,并小于它右子树所有的节点值。

如上图4.1是一个二叉树,基于以上特点,我们可以采用链表的搜索方式查找二叉树,并将“指针的指针通过间接操作符*改变指针指向的指针”这一思想运用到其中。

例:在上图二叉树插入11.

结果如图4.2所示:

图4.2

节点结构为:

typedef struct  TREENODE {

int value;

struct  TABLENODE*left;

struct  TABLENODE*right;

}TreeNode;

代码如下:

void insert(TreeNode** node,int value)

{

    assert(node!=NULL);//

    TreeNode*current;//当前节点

    TreeNode*insertnode;//要插入的节点

    current= *node;

    while((current!=NULL))

    {

        if(current->value>value)

        {

            node=¤t->left;

            current=*node;

        }

        else

        {

            if(current->value

            {

                node=¤tt->right;

                current=*node;

            }

        }

    }

//新增:

    insertnode=(TreeNode*)malloc(sizeof(TreeNode));

    insertnode->left=NULL;

    insertnode->right=NULL;

    insertnode->value=value;

    *node=insertnode;

}

参考书籍:《C和指针》《C专家编程》《STL源码剖析》

你可能感兴趣的:(指针的指针应用||数据结构)