链表的相关操作

来源:互联网 发布:北京优化新材料 编辑:程序博客网 时间:2024/05/16 06:28
在学习数据存储的时候,我们就曾经遇到过两种存取方式,第一种是使用连续的数组来存放数据,还有一种便是使用链表的方式来进行存储。
一般来说,使用数组来存放数据的时候我们常常会遇到的问题就是以下:
  • 存储空间需要预先指定,这样一来就对那些本来不是很明确需要多少存储空间的数据带来了一定的麻烦,如果我们在指定的时候分配了较少的空间,则相当有可能导致数组越界的问题的出现;而当我们稳妥起见分配了较多空间的时候,事实上又是一种浪费;
  • 数组的插入比较单一,只能选择将数据插在数组的尾部;
  • 最为致命的问题就是数组在处理删除问题上,将会相当麻烦!我们想要删除一个数据,如果这个数据本来是在数组的最后,那么这种情况还能够让人接受,直接删除该数据就好了;但是如果我们想要删除的数据在数组中,那么问题就来了,我们不仅要删除而且,还要将待删除后面的数据一个个向前移动,这样的效果是极其差的!
  • ……

一般来说鉴于以上的种种原因,我们更大程度上愿意使用链表这一数据结构来完成对数据的存储,链表是将每个带存放的数据封装在一个结点内,该结点的实现大致如下:

/*** 定义一个结点类,其中data用来存放待存放的数据,其类型可以是我们想要存放的各种类型* 而next指针则是用来指向下一个结点的指针*/class Node{public:     Node(int x){        data = x;        next = nullptr;     }    ~ Node();    int getData();    Node* getNext();    void setData(int x);    void setNext(Node* node);private:    Node *next;    int data;};
由以上的Node类的定义我们可以明白,在链表中我们数据的存放大致是怎样进行的
——由data负责放置数据,而next则是用于指向下一个结点的指针。
这就告诉我们链表的存储将不会是像数组那样需要分配连续的存储空间来存放数据,相反,链表的空间是不连续的,正是由于这个特性,我们在处理在特定的数据(结点)后插入新结点,删除某一结点就变得相对容易很多。
我们先来看看链表类,对于一个链表我们要实现的操作主要有:
  • 插入(insert)
  • 删除(remove)
  • 查询(find)
  • 长度(size)
  • 遍历(traversal)
  • 排序(order)
  • 全部删除(deleteAll)
/*** 链表的类定义* 私有成员的中的两个指针,其中root就是整个链表的头结点,* 而len则是整个链表的长度*/class List{public:    List(){        root = nullptr;        len = 0;    }    ~List(){        delete root;    }    int size();    Node* insert(int x);    bool remove(int x);    Node* find(int x);    bool deleteAll();    void order();    void traversal();    void setHead(Node* node);private:    Node* root;    Node* head;    int len;};
我们看以上的链表类类定义,可能大家发现了我在私有成员定义的时候定义了两个头结点指针——head和root,那么,这两个都是头结点,究竟有什么区别呢?
事实上,由于我在这里使用的是尾插法向链表中插入数据,需要时时刻刻记录链表的头指针,但是尾插法在使用的时候除了头指针外还需要两个指针迭代插入。因此我将root设置为在插入过程中慢慢的跟着链表动;而head也是整个链表的头结点,head和root的唯一区别就是,head始终指向链表头,而root会随着插入而不断移动。
接下来我们来看看链表的插入操作:
/*** 链表类的插入函数* 在插入的同时完成对链表长度的计数*/Node* List::insert(int x){    Node* node = new Node(x);    if (root == nullptr)    {        root = node;    }else{        root -> next = node;        root = node;    }    ++ len;    return root;}
在链表的插入函数中,我们不必考虑越界问题(除非计算机的内存全部用完,不过这种情况还是相当罕见的),我们需要考虑的问题主要有两点:
  1. 当我们刚new一个新的链表对象的时候,里面的数据为空,头结点也为空,这个时候的插入应当是怎样的?
  2. 当我们的头结点非空的时候,我们有应当怎样插入结点?
对于第一个问题,因为头结点为空嘛,所以我们直接将插入的数据使用构造函数封装成为一个结点对象,直接赋值给头节点就好了。
对于第二个问题,我们采用的就是以下的方法来进行插入的操作,

list&

    (在这里我要说我原来犯过的一个错误,一个bug。在刚开始我在写Node结点类的时候,为了安全起见,我是将data和next两个变量设置为私有成员的,就像以下的情况:
// version 1.0class Node{public:     Node(int x){        data = x;        next = nullptr;     }    ~ Node();    int getData();    Node* getNext();private:    Node *next;    int data;};
    然后,正当我兴高采烈的时候,相当兴奋的先编译一波,结果却得到以下的报错:  expression is not assignable!!
D:\Program Files\LLVM>clang++ C:\Users\50萌主\Desktop\demo.cpp -o     C:\Users\50萌主\Desktop\demo.exe     C:\Users\50钀屼富\Desktop\demo.cpp:68:21: error: expression is not assignable                root -> getNext() = node;                ~~~~~~~~~~~~~~~~~ ^C:\Users\50钀屼富\Desktop\demo.cpp:82:21: error: expression is not assignable                head -> getNext() = nullptr;                ~~~~~~~~~~~~~~~~~ ^C:\Users\50钀屼富\Desktop\demo.cpp:124:26: error: expression is not assignable                        previous -> getNext() = node -> getNext();                        ~~~~~~~~~~~~~~~~~~~~~ ^C:\Users\50钀屼富\Desktop\demo.cpp:148:23: error: expression is not assignable                                temp -> getData() = tem -> getData();                                ~~~~~~~~~~~~~~~~~ ^C:\Users\50钀屼富\Desktop\demo.cpp:149:22: error: expression is not assignable                                tem -> getData() = i;                                ~~~~~~~~~~~~~~~~ ^5 errors generated.
   就很难受,明明我在使用结构体来定义Node的时候就啥事儿都没有啊???!!!!!   后来官网查询之后发现这个错误是因为,错误地对“右值”进行赋值操作!我当时就相当想的不明白,其他的方法也不能解决,后来索性直接将这两个变量的权限设置为public,并删除了相应的get方法,也就是以下的代码       
// version 1.1class Node{public:     Node(int x){        data = x;        next = nullptr;     }    ~ Node();    Node *next;    int data;};
    后来越来越觉得设置为public权限确实不安全,但还是想不明白,于是又一次索性去看了看Java,这一个不小心就看到Java中的类的定义,有变量,有get方法,还有对应的set方法!!!什么?set方法!!set方法!!    脑海突然灵光一现,咱们原来赋值不是通过set方法来进行的么???什么时候对get方法获取的值赋值过啊。这下全部明白了,原来get方法返回的值是一个“右值”,因此不能被用于赋值。原来闹了半天的bug原来是自己太不小心导致的……    于是就有了第三个版本,这次很安全的版本:
// version 1.2class Node{public:     Node(int x){        data = x;        next = nullptr;     }    ~ Node();    int getData();    Node* getNext();    void setData(int x);    void setNext(Node* node);private:    Node *next;    int data;};

)

然后,我们来看看怎样实现链表中的寻找,也即查询方法——
主要的思路就是从头结点开始,然后按照链表的指针指向顺序开始遍历,当找到第一个与待查找的数据的值相等的时候,就跳出遍历,同时返回包含这个数值的结点,若全部遍历完仍然没找到待查找数据,则将返回一个空指针。
以下是代码实现:
/***  查找函数(准确来说,是查询链表中第一个)*  返回含有待查值的结点*/Node* List::find(int x){    Node* node = new Node(x);    Node* temp = head;    while(temp != nullptr){        if (temp -> getData() == x){                return temp;        }else{            temp = temp -> getNext();            continue;        }    }    return nullptr;}
接下来,我们就来看看数据的删除是怎样实现的。
/*** 删除函数,首先需要找到待删除数据的结点,* 找到结点之后,分成两种情况,其中一种是删除头结点的情况* 还有一种是删除非头结点的情况* 同时在删除成功的时候,将链表的长度减少*/bool List::remove(int x){    Node* node = find(x);    if (node == nullptr)        return false;    else{        /*if the node that you want to remove is the root node*/        if (head -> getData() == x){            head = head -> getNext();        }else{            /* find the aim target's previous node */            Node* temp = head;            Node* previous;            while(temp != nullptr){                if ((temp -> getNext()) -> getData() == x){                    previous = temp;                    break;                }                temp = temp -> getNext();            }            // previous -> getNext() = node -> getNext();            previous -> setNext(node -> getNext());        }    }    -- len;    return true;}
对于以上的的两种情况,
  1. 当我们查找之后发现待删除的是头结点的时候,这样就比较简单,在这种情况下,我们直接将头结点的指针向后移动一位就可以了;
  2. 当我们待删除的是非头结点的时候,这时就需要我们先找到待删除结点的前一个指针(在这里我们只能通过从头到尾的遍历,才能找到他的前驱。但是如果我们采用的是双向链表的话将会很方便的找到前驱),然后再按照以下图示的方法来完成删除操作。
    list remove
完成了以上的这些对单个结点的操作之后,我们开始全局对整个链表进行操作:
首先我们开始看看遍历操作:
遍历操作比较简单,主要的难点集中在确定整个链表的头结点上,想必大家还记得我在定义链表属性的时候,有定义过一个head和root两个头结点,而head就是我们遍历时所需要的,因此我们要想办法使得head指针的指向就是链表的头结点,怎样实现呢?
我们当时将insert方法的返回类型,设置为Node*就是为了给head赋值;
根据我们当时的插结点算法,我们不难知道当我们刚创建一个新的链表对象,并且插入第一个值之后返回的就是头结点,所以,我们使用setHead方法来实现对head的赋值(这次我们使用的是set函数来实现的,没有对get方法直接赋值)——
List* list = new List();list -> setHead(list -> insert(5));
获取了head头结点之后,现在我们就可以开始遍历了:
// 遍历函数void List::traversal(){    Node* temp = head;    while(temp != nullptr){        cout << temp -> getData() << " ";        temp = temp -> getNext();    }}
细心的小伙伴应当发现了,我在遍历的时候并没有直接使用head作为头结点进行直接遍历,反而是创建了一个临时对象,使用的是这个临时对象来进行遍历。原因是什么呢?
是因为如果我们直接使用head进行遍历的话,那么事实是遍历完一遍之后,head就直接指向最后一个了。因此我们需要新建一个临时对象来实现遍历操作。
好,接下来我们就做一件相当刺激的事儿,完成这个相当刺激的功能,那就是删除全部元素。
这一功能比较简单,总的来说就是让head的next指针指向nullptr,然后再让head本身指向nullptr。
// 删除所有bool List::deleteAll(){    if (head == nullptr)    {        return false;    }else{        // head -> getNext() = nullptr;        head -> setNext(nullptr);        head = nullptr;    }    len = 0;    return true;}
最后我们来实现一个排序的功能,这个函数只能用于那些能够使用>、<符号比较大小的数据类型上,对于自定义的数据需要重载运算符才能使用,这里我们不能使用快排,在这里我们使用冒泡排序来实现链表的排序,代码如下:
/*Bubble Sort*/void List::order(){    Node* temp = head;    while (temp -> getNext() != nullptr){        for (auto tem = temp -> getNext(); tem != nullptr ; tem = tem -> getNext()){            if (temp -> getData() > tem -> getData()){                int i = temp -> getData();                // temp -> getData() = tem -> getData();                temp -> setData(tem -> getData());                // tem -> getData() = i;                tem -> setData(i);            }        }        temp = temp -> getNext();    }}
这就大致完成了整个链表的大致全部功能。
以上。
原创粉丝点击