数据结构(1)——链表的基本操作

1.链表的基本概念

  • 概念:链表是一种线性的数据结构,通过指针将零散的内存块连接起来,链表的没个内存块成为节点。
  • 链表的实现方法:链表以结构体为节点(包含数据域和指针域),利用数据域来存储数据,然后将每个节点的指针域都指向下一个节点,以此来实现数据的存储。
//节点示例:
typedef struct node{
    
    
    int num;//数据域
    struct node * next;//指针域
} Node;
  • 链表的优缺点(相较于数组而言):
    优点:可以不断扩展链表的长度,删除和插入操作比较方便。
    缺点:创建,查找操作较为麻烦。
  • 图示:
    在这里插入图片描述
    说明:上图所示的链表是一个无头节点的单链表,关于链表有无头节点的问题我们会在3.3.1中讨论。

2.创建链表

2.1尾插法

  • 操作:将新节点不断地插入链表的末尾从而来创建链表。

  • 特点:数据的输入顺序与链表的存储顺序相同。

  • 代码示例:

Node * create(){
    
    
    Node *head,*new,*end;
    head=end=(Node *) malloc(sizeof(Node));
    int x;
    while(scanf("%d",&x)==1&&x!=-1){
    
    //当输入-1时循环结束
   	 	//准备新节点
        new=(Node *) malloc(sizeof(Node));
        new->num=x;
        new->next=NULL;
        
        end->next=new;//将新节点连接到链表上
        
        end=new;//更新尾结点
    }
    return head;
}

2.2头插法

  • 操作:每次都会将新节点插在头结点之后。
  • 特点:数据的输入顺序与链表的存储顺序相反。
  • 代码示例:
Node * create(){
    
    
    Node *head,*new;
    head=(Node *) malloc(sizeof(Node));
    head->next=NULL;//保障末尾节点的指针域指向NULL
    int x;
    while(scanf("%d",&x)==1&&x!=-1){
    
    
    	//准备新节点
        new=(Node *) malloc(sizeof(Node));
        new->num=x;
        new->next=head->next;

		//将新节点连接到链表上
        head->next=new;
    }
    return head;
}

3.链表的遍历及其增删改查

3.1链表的遍历

  • 思路:先定义一个节点让它等于链表的第一个节点,再在遍历时不断对其进行判断,若为NULL则链表到头了应结束循环,若不为NULL则该节点等于链表的下一个节点并继续循环。
  • 代码示例:
Node *head=create();
Node *q=head->next;
while(q!=NULL){
    
    
    printf("%d ",q->num);
    q=q->next;//移动节点
}

3.2增加新节点

  • 思路:遍历链表找到要插入位置的前一个节点,操作指针插入新节点即可.
  • 代码示例:
void insert(Node ** head,int number,int data){
    
    
    int i=0;
    Node *q=(Node *) malloc(sizeof(Node));
    q->num=data;
    Node *p=head;
    while(p!=NULL){
    
    
        if(i==number){
    
    
            q->next=p->next;
            p->next=q;
            break;
        }else{
    
    
            i++;
            p=p->next;
        }
    }
}

说明:该函数第一个参数是链表头的地址,第二个参数是要插入的位置,第三个参数是新节点的数据。而第一个参数之所以是链表头的地址而非是链表头是因为:假若你想要插入到第0个节点之后(第一个节点之前),那么就要操作头结点的指针了,为了使在函数中的操作能影响到链表头,所以传入的是链表头的地址。

3.3删除节点

3.3.1按位删除节点

  • 思路:删除节点的关键在于找到要删除节点的前驱节点,然后通过改变前驱节点的指针域来完成删除操作。
  • 代码示例:
void delete(Node ** head,int number){
    
    //与3.2同理
    int i=1;
    Node *p=head,*q=p->next;
    while(q!=NULL){
    
    
        if(number==i){
    
    
            p->next=q->next;
            free(q);//释放内存
            break;
        }else {
    
    
            i++;
            p=q;
            q=q->next;
        }
    }
}

说明:这里我们就要讲解一下链表有无头结点的区别了,从开始到现在我们使用的都是有头结点的链表,而头结点一般是不存放数据的,那为什么还要有头结点呢?

事实上头结点的存在是为了方便我们操作链表的,就比如说删除节点的这一操作:假设我现在要删除的就是第一个节点,那么按照思路我么也是要先找到它的前驱节点的,而倘若是无头节点的链表,第一个节点前也就不存在节点;而倘若是有头结点的链表就会轻而易举的得到它的前驱节点了。

3.3.2按数据删除节点

  • 思路:和3.3.1一样的原理,不过这里我们要注意:我们删除的可能不止一个节点,而当我们删除了一个节点之后,我们维系的前后指针关系就会被破坏,因此每当我们删除一个接点之后要及时恢复前后指针的关系。
  • 代码示例:
void delete(Node ** head,int number){
    
    
    Node *p=head,*q=p->next;
    while(q!=NULL){
    
    
        if(q->num==number){
    
    
            p->next=q->next;
            free(q);

            q=p->next;//恢复前后节点的关系
        }else {
    
    
            p=q;
            q=q->next;
        }
    }
}

3.4修改数据

  • 思路:遍历链表找到需要改变数据的节点修改其数据即可。
  • 代码示例:相信你自己一定能够实现,我这里就省略了。

3.5查找数据

  • 思路:遍历链表根据条件进行查找即可。
  • 代码示例:略.

4.链表的升序合并,冒泡排序,逆置

4.1升序合并

  • 思路:定义两个指针指向两个链表的首节点,再拿这两个节点进行比较,找出较小的节点存储起来,然后指向该链表的指针后移一位。重复上述操作,直到有一个指针为空,再将另一个链表中的剩余节点补在已存储节点之后即可。
  • 图示:
    在这里插入图片描述
  • 代码示例:
//由于思路中有重复操作的思想所以这里采用递归的方法实现
Node * merge(Node *head1,Node *head2){
    
    
    if(head1==NULL){
    
    
        return head2;
    }else if(head2==NULL){
    
    
        return head1;
    }else {
    
    
        if(head1->num<head2->num){
    
    
            head1->next=merge(head1->next,head2);
            return head1;
        }else {
    
    
            head2->next=merge(head1,head2->next);
            return head2;
        }
    }
}

注意:该函数的两个参数均为无头结点的链表,返回值也是一个无头节点的链表。

4.2冒泡排序

  • 思路:与数组的冒泡排序思想基本一致,不过这里在做交换时,可以交换节点也可以交换数据,其中最容易出错的就是交换节点了。废话不多说直接上代码。
  • 代码示例(交换节点):
void sort(Node * head){
    
    
    Node *p,*prep,*tail;//p为当前节点,prep为p的前驱节点,tail控制循环的深度
    tail=NULL;
    while(head->next!=tail){
    
    
        prep=head;
        p=head->next;
        while(p->next!=tail){
    
    
            if(p->num>p->next->num){
    
    
            	//这里只需先删除p节点,再往p->next后插入一个p节点即可
                prep->next=p->next;
                p->next=p->next->next;
                prep->next->next=p;
                //当你交换了节点之后prep和p的前后关系就被打破了,这里要做调整
                prep=prep->next;
                continue;
            }
            p=p->next;
            prep=prep->next;
        }
        tail=p;//p之后的节点已为有序
    }
}

4.3逆置

  • 思路:我们需准备好当前节点的前驱结点以及一个用于暂存的节点,我们可以用暂存节点暂存当前节点的下一节点,然后将当前节点指向前驱结点,前驱结点后移,再利用暂存节点继续进行循环直至链表结束。
  • 代码示例:
Node * reverse(Node *head){
    
    
    Node *pre,*temp;
    pre=NULL;
    while(head!=NULL){
    
    
        temp=head->next;//暂存下一节点
        head->next=pre;//将当前节点指向前驱节点
        pre=head;//前驱节点后移
        head=temp;//利用暂存节点继续进行循环
    }
    return pre;
}

注意:该函数的参数为无头节点的链表,返回的也是一个无头节点的链表。

猜你喜欢

转载自blog.csdn.net/ABded/article/details/108738896
今日推荐