我的数据结构-第七章

来源:互联网 发布:彩虹六号低配优化补丁 编辑:程序博客网 时间:2024/04/27 11:36

第七章 链表、栈、队列

从本章开始进入程序设计的灵魂——数据结构,由此可见数据结构在程序设计中的重要程度。数据结构是相互之间存在一种或多种特定关系的数据元素的集合,通常有4类基本结构:集合、线性结构、树形结构、图状结构。本书是站在ACM程序设计角度来编写的所以主要侧重数据结构中的算法设计,对于一些定义会简单带过如果要想深入了解请查阅相关的资料。

7.1 链表

7.1.1 什么是链表

链表是一种物理存储单元上非连续、非顺序的存储结构。是一种常见的基础数据结构,它是线性表,但是并不会按照线性的顺序存储数据,它的数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域,如图7.1所示。

7.1.2 链表的表示

根据链表的定义由数据域跟指针域组成因此我们可以写出下面的链表的一般结构:

struct Node{

int data;//数据域

struct Node * next;//指针域

};

其中Node是定义,data就是一个数据域,而*next就是指针域用来指向下一个链表。

7.1.3 链表的基本操作

链表的基本操作有:创建、查找、插入、删除和修改等。

(1)创建链表:从无到有地建立起一个链表,如下所示。

struct Node{

    int num;

    struct Node *next;

};

Node *P = new Node;

(2)查找:按给定的结点索引号或检索条件,查找某个结点。如果找到指定的结点,则称为检索成功;否则,称为检索失败,如下所示。

Node *Headl;

Head = P;

While(Head != NULL){

...

Head = P->next;

}

(3)插入:在结点ki-1与ki之间插入一个新的结点k’,使表的长度增1,且逻辑关系发生如下变化:

插入前,ki-1是ki的前驱,ki是ki-1的后继;

插入后,新插入的结点k’成为ki-1的后继、ki的前驱,如图7.2所示。

(4)删除操作:删除结点ki,使链表的长度减1,且ki-1、ki和ki+1结点之间的逻辑关系发生如下变化:

删除前,ki是ki+1的前驱、ki-1的后继;

删除后,ki-1成为ki+1的前驱,ki+1成为ki-1的后继,如图7.3所示。

7.1.4 链表的优劣势

既然链表是一种方便的数据存储结构,那么我们在什么情况下使用链表呢?

根据链表的定义我们可以得出在数据插入删除较多的情况下使用链表可以大大的提高效率。由于不必按顺序存储,链表在插入的时候可以达到O(1)的时间复杂度比顺序表快的多,但是在查找一个节点或者访问某个特定的节点时则需要O(n)的时间复杂度需要从头开始去查找,此种情况顺序表只需要O(1)。删除时候的时间复杂度跟插入是一样的。使用链表还有一个优势,就是在一串序列中我们并不知道数据额的大小,此时如果我们采用顺序存储的方式我们就没法去开空间的大小,如果使用链表则不用去考虑,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了随机读取的优点,同时链表由于增加了结点的指针域,空间开销会比较大。因此我们在进行算法设计的时候要根据数据的特性去选择相对较优的方法。

7.1.5 链表在算法中的应用

前面讲了这么多关于链表的知识,那么现在我们就来看看链表在程序设计中是怎么实现的,通过例题来讲解。

例7.1 开火车

火车是大家再熟悉不过的交通工具了,大家有没注意火车的车厢,是不是一个一个链起来的,对,其实它就是一个链表。现在我们用程序来实现火车车厢的增加、删除、查找,假定开始时有5个车厢。

分析:此题是一个典型的链表结构题,考查的就是对链表的基本操作。我们可以先定义一个链表,然后不断往后面添加,在删除操作时先找到要删除的结点,然后将此结点的前一个结点的指针域指向删除结点的下一个结点,这样该结点就变成了孤立的点即实现了删除,查找时我们从链表的头开始一直往next查找,直到找到为止。

程序:

struct Node{

    int num;

    struct Node *next;

};

Node *train;            //train

Node *curent;           //current Add Node

void addNode(Node *p){  //Add Train Node

    if(train == NULL)

        train = p;

    else{

        curent->next = p;

    }

    curent = p;

    cout << "Add A Node is " << p -> num << endl;

}

void printNode(){       //Print Train

    Node *p;

    p = train;

    if(p == NULL){

        cout << "The Train Is NULL" << endl;

        return;

    }

    bool flag = false;     //Del The Last Blank

    while(p!=NULL){

        if(flag)

            cout << " ";

        cout << p->num;

        p = p->next;

        flag = true;

    }

    cout  << endl;

}

void findNode(int num){

    Node *p;

    p = train;

    while(p!=NULL){

        if(p->num == num){

            cout <<num << " Is Find:" << endl;

            return;

        }

        p = p->next;

    }

    cout << num << " Is Not Find" << endl;

}

void delNode(int num){

    Node *p;

    Node *pre;

    p = train;

    pre = NULL;

    while(p!=NULL){

        if(p->num == num){

            if(pre != NULL){

                pre->next = p->next;

            }else{          //Node is Head Node

                train = p->next;

            }

            cout << num << " Node Del Success"<<endl;

            return;

        }

        pre = p;

        p = p->next;

    }

    cout << num << "Node Del Failed" << endl;

}

注意:在此程序中需要注意的地方有两个:一是当我们要插入或者删除的结点在链表的头的时候,这时候我们需要特殊判断,很多同学在这里容易出错。二是当我们在使用链表时一定要记得给它分配空间。

7.2 链表查找合并

给定两个链表AB,分别由NN<100)和M(M<100)个元素组成,然后找出A链表中查找B链表的元素,若查找不到则将该元素添加到A链表中,最后输出A链表。

样例输入:

5

1 2 3 4 5

3

0 2 3 

样例输出:

1 2 3 4 5 0

分析:此题目其实就是对链表进行查找和插入然后输出元素的操作,我们可以采用火车这题的思路,不同的是链表里面的元素是需要自行输入的。具体的步骤可以分为以下几步:

a、初始化AB链表。

b、输入元素,构建链表。

c、在链表A中查找每一个B的元素,若存在不操作,若不存在加入链表A的末尾。

d、输出链表A

程序:

struct Node{

int data;

struct Node *next;

};

Node *A;

Node *B;

void creatLink(){

int n;

cin >>n;

Node *p = new Node;

for(int i=0;i<n;i++){

Node *tmp = new Node;

cin >> tmp->data;

if(A == NULL)

A = tmp;

else

p->next = tmp;

p = tmp;

}

p->next = NULL;

cin >> n;

Node *q = new Node;

for(int i=0;i<n;i++){

Node *tmp = new Node;

cin >> tmp->data;

if(B == NULL)

B = tmp;

else

q->next = tmp;

q = tmp;

}

q->next = NULL;

}

void insert(int data){

Node *tmp = new Node;

tmp->data = data;

tmp->next = NULL;

if(A == NULL){

A = tmp;

return ;

}

Node *p,*pre;

p = A;

while(p != NULL){

pre = p;

p = p->next;

}

pre->next = tmp;

tmp ->next = NULL;

}

bool find(int data){

Node *p;

p = A;

while(p!=NULL){

if(p->data == data)

return true;

p = p->next; 

}

return false;

}

void merge(){

if(B == NULL)

return ;

Node *q;

q = B;

while(q!=NULL){

if(!find(q->data)){

insert(q->data);

}

q = q->next;

}

}

void print(Node *tmp){

Node *p;

p = tmp;

bool flag = false;

while(p != NULL){

if(flag) cout << " ";

cout << p->data;

p = p ->next;

flag = true;

}

cout << endl;

}

int main(){

A = new Node;

B = new Node;

A = NULL;B = NULL;

creatLink();

merge();

print(A);

}

注意:一、此题在插入和查找时容易忽略链表为空的情况。二、通过此程序我们就可以看出链表在查找的时候是极为的不方便的,如果采用顺序存储的方式我们可以使用标记数组的办法使查找的速度达到O(1)。对于这种方法还不了解的同学请看前面数组一章的例题。此方法对于查找数值不大的数据是非常有效的。

7.3 约瑟夫问题

N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6M=5,被杀掉的人的序号为54623。最后剩下1号。现在给定NM由你写出程序计算出被杀的人的顺序,以及最后留下的人的序号。

样例输入:

6 5

样例输出:

5 4 6 2 3 1

分析:此题在前面的章节中我们采用数组讲解过,这里我们采用新的方法——链表来解这道题。思路跟前面章节是一样的就不在详细讲述了,这里要注意的是N个人事围成一圈的,这样有些同学就会认为这不合符链表的思路,其实不然,这里我们可以采用循环链表,即链表的尾部不再指向空指针还是指向第一个,这样不就实现了一圈的思想了吗?下面就来看看这道题怎么写吧:

程序:

struct Person{

int num;

struct Person *next;

};

Person *person;

void init(int n){

if(n < 1) return ;

Person *cur;

for(int i=1;i<=n;i++){

Person *p = new Person;

p->num = i;

if(person == NULL)

person = p;

else

cur->next = p;

cur = p;

}

cur->next = person;

}

void work(int n,int m){

Person *pre;

Person *p = new Person;

pre = new Person;

pre = NULL;

p = NULL;

while(true){

if(p == NULL)

p = person;

if(n == 1){ 

cout << p->num << endl;

return ;

}

int cur = 1,kill;

while(true){

if(cur == m){

if(pre != NULL)

pre->next = p->next;

else

person = p->next;

kill = p->num;

p = p->next;

break;

}

pre = p;

cur ++ ;

p = p->next;

}

cout << kill << " ";

n -- ;

}

}

int main(){

int n,m;

while(cin >> n >> m){

person = new Person;

person = NULL;

init(n);

work(n,m);

}

}

注意:这个算法的时间复杂度为O(n),相对于模拟算法已经有了很大的提高。算nm等于一百万,一千万的情况不是问题了。可见,适当地运用数学策略,不仅可以让编程变得简单,而且往往会成倍地提高算法执行效率。具体解法会在提高篇中讲述。

7.1.6 本节小结

0 0