[数据结构] 链表

写在前面

菜鸡博主开始复习了,先从数据结构开始吧(其实是每天复习高数太累了)

1. 单链表

单链表是线性表的链式存储,是指通过一组任意的存储单元来存储线性表中的数据元素。对每个链表节点,除了存放元素自身的信息之外,还需要存放一个指向其后继的指针(如下图所示)

单链表的节点可以用如下代码描述:

C++ 复制代码
typedef struct Node {
	int data;
	struct Node *next;
}Node, *LinkedList; 
// Node表示节点的类型,LinkedList表示指向Node节点类型的指针类型

1)单链表的初始化

初始化主要完成以下工作:创建一个单链表的前驱节点并向后逐渐逐步添加节点,用代码可以表示为:

c++ 复制代码
LinkedList InitList() {
	Node *L;
	L = (Node *)malloc(sizeof(Node)); // 创建头节点,开辟内存
	if(L == NULL)
        printf("申请内存失败");
    L->next = NULL;
}

[注]:判断内存是否开辟失败是很有必要的,虽然可以省略不写!

2)头插法创建单链表

利用指针向下一个节点元素的方式进行逐个创建,得到的结果是逆序的,如图所示:

从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点之后。

c++ 复制代码
LinkedList HeadCreatedList() {
    Node *L;
    L = (Node *)malloc(sizeof(Node));
    L->next = NULL;
    
    int x; // x为链表数据域中的数据
    while(scanf("%d", &x) != EOF) {
        Node *p;
        p = (Node *)malloc(sizeof(Node)); // 申请新节点
        p->data = x; // 节点数据域赋值
        p->next = L->next; // 将节点插入到表头 L --> |2| --> |1| --> NULL
        L->next = p;
    }
    return L;
}

3)尾插法创建单链表

头插法生成的链表中,结点的次序和输入数据的顺序不一致。若希望两者次序一致,则需要尾插法。

必须增加一个尾指针r,使其始终指向当前链表的尾节点

c++ 复制代码
LinkedList TailCreatedList() {
    Node *L;
    L = (Node *)malloc(sizeof(Node));
    L->next = NULL;
    Node *r;
    r = L; // r开始时指向头节点
    int x;
    while(scanf("%d", &x) != EOF) {
        Node *p;
        p = (Node *)malloc(sizeof(Node));
        p->data = x;
        r->next = p;
        r = p;
    }
    r->next = NULL;
    return L;
}

4)遍历单链表

遍历输出的思路:建立一个指向链表L的节点,沿着链表L向后搜索

c++ 复制代码
void PrintList(LinkedList L) {
    Node *p = L->next;
    int i = 0;
    while(p) {
        printf("第%d个元素的值为%d", ++ i, p->data);
        p = p->next;
    }
}

我们也可以把其中的某个值x改成k

c++ 复制代码
LinkedList ReplaceList(LinkedList L, int x, int k) {
    Node *p = L->next;
    int i = 0;
    while(p) {
        if(p->data == x)
            p->data = k;
        p = p->next;
    }
    return L;
}

5)插入、删除

链表的插入操作主要分为查找到第i个位置,将该位置的next指针修改为指向我们新插入的节点,而新插入的节点next指针指向我们i+1个位置的节点。

其操作方式可以设置一个前驱节点,利用循环找到第i个位置,再进行插入。

如图,在DATA1和DATA2数据节点之中插入一个NEW_DATA数据节点:

c++ 复制代码
LinkedList ListInsert(LinkedList L, int i, int x) {
    Node *pre = L; // 指针p指向当前扫描到的节点
    int j = 0;
    while(pre != NULL && j < i - 1) { // 循环找到第i-1个节点
        pre = pre->next;
        j ++;
    }
    if(p == NULL)
        return 0;
    Node *s = (Node *)malloc(sizeof(Node));
    s->data = x;
    s->next = pre->next;
    pre->next = s;
    return L;
}

删除元素要建立一个前驱结点和一个当前结点,当找到了我们需要删除的数据时,直接使用前驱结点跳过要删除的结点指向要删除结点的后一个结点,再将原有的结点通过free函数释放掉。如图所示:

c++ 复制代码
LinkedList ListDelete(LinkedList L, int x) {
    Node *p, *pre; // pre为前驱,p为查找的节点
    p = L->next;
    while(p->data != x) {
        pre = p;
        p = p->next;
    }
    pre->next = p->next;
    free(p);
    return L;
}

6)合并递增单链表

c++ 复制代码
struct ListNode* mergeLists(struct ListNode* l1, struct ListNode* l2) {
    struct ListNode dummy;      // 创建虚拟头节点
    struct ListNode* tail = &dummy; // 创建尾节点指针
    while (l1 != NULL && l2 != NULL) { // 遍历两个链表
        if (l1->val < l2->val) { // 如果l1的值小于l2的值
            tail->next = l1;    // 将l1添加到新链表中
            l1 = l1->next;      // 移动l1指针到下一个节点
        } else {                // 如果l2的值小于等于l1的值
            tail->next = l2;    // 将l2添加到新链表中
            l2 = l2->next;      // 移动l2指针到下一个节点
        }
        tail = tail->next;      // 移动尾节点指针到新链表的尾部
    }
    if (l1 != NULL) {           // 如果l1还有剩余节点
        tail->next = l1;        // 将剩余节点添加到新链表的尾部
    } else {                    // 如果l2还有剩余节点
        tail->next = l2;        // 将剩余节点添加到新链表的尾部
    }
    return dummy.next;          // 返回新链表的头节点
}

7)逆序

直接给出完整可运行代码叭

c++ 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<time.h>

struct ListNode {
    int val;
    struct ListNode *next;
};

// 创建一个包含n个随机整数的单链表
struct ListNode * createList(int n) {
    struct ListNode *head = NULL, *tail = NULL;
    for(int i = 0; i < n; i ++) {
        struct ListNode *node = (struct ListNode *)malloc(sizeof(struct(ListNode)));
        node->val = rand() % 100; // 随机生成一个0-99的整数作为节点的值
        node->next = NULL;
        if(tail == NULL)
            head = tail = node; // 链表为空,则将头指针和尾指针指向新节点
        else {
            tail->next = node;
            tail = node;
        }
    }
    return head;
}

// 打印链表
void printList(struct ListNode *head) {
    while(head != NULL) {
        printf("%d ", head->val);
        head = head->next;
    }
    printf("\n");
}

// 逆序
void reverseList(struct ListNode **head) {
    if(*head == NULL || (*head)->next == NULL)  {
        //若链表为空或只有一个节点
        return ;
    }
    struct ListNode *prev = NULL, *curr = *head, *next = NULL;
    while(curr != NULL) { // 遍历链表,将每个节点的后继指针指向前一个节点
        next = curr->next;
        curr->next = prev;
        prev = curr;
        curr = next;
    }
    *head = prev; // 将头指针指向逆序后的链表头节点
}

int main() {
    srand(time(NULL));
    struct ListNode *L = createList(5);
    printf("The original list is: ");
    printList(L);
    reverseList(&L);
    printf("The new list is: ");
    printList(L);
    return 0;
}

2.双链表

其中,DATA表示数据,其可以是简单的类型也可以是复杂的结构体;

pre代表的是前驱指针,它总是指向当前结点的前一个结点,如果当前结点是头结点,则pre指针为空;

next代表的是后继指针,它总是指向当前结点的下一个结点,如果当前结点是尾结点,则next指针为空

结构体定义为:

C++ 复制代码
typedef struct line {
    int data;
    struct line *pre;
    struct line *next;
}line, *a;

1)创建

C++ 复制代码
line *InitLine(line *head) {
    // 节点数量,当前位置,输入的数据
    int number, pos = 1, inputdata;
    printf("请输入创建节点的大小");
    scanf("%d", &number);
    printf("\n");
    if(number < 1)
        return NULL;
    head = (line *)malloc(sizeof(line));
    head->pre = NULL;
    head->next = NULL;
    printf("输入第%d个数据\n",pos ++);
    scanf("%d", inputdata);
    head->data = inputdata;
    
    line *list = head;
    while(pos <= number) {
        line *body = (line *)malloc(sizeof(line));
        body->pre = NULL;
        body->next = NULL;
        printf("输入第%d个数据\n", pos ++);
        scanf("%d", &inputdata);
        body->data = inputdata;
        
        list->next = body;
        body->pre = list;
        list = list->next;
    }
    return head;
}

2)插入

C++ 复制代码
line *InsertLine(line *head, int data, int location) {
    line *temp = (line *)malloc(sizeof(line));
    temp->data = data;
    temp->pre = NULL;
    temp->next = NULL;
    
    if(location == 1) {
        temp->next = head;
        head->pre = temp;
        head = temp;
    } else {
        line *body = head;
        for(int i = 1; i < location )
            body = body->next;
        // 若插入位置在链表尾
        if(body->next == NULL) {
            body->next = temp;
            temp->pre = body;
        } else {
            body->next->pre = temp;
            temp->next = body->next;
            body->next = temp;
            temp->pre = body;
        }
    }
    return head;
}

3)删除

C++ 复制代码
line *DeletaLine(line *head, int data) {
    line *list = head;
    while(list) {
        if(list->data == data) {
            list->pre->next = list->next;
            list->next->pre = list->pre;
            free(list);
            return head;
        }
        list = list->next;
    }
    printf("未找到该元素,删除失败");
    return head;
}

再给出一个题目的代码

实现程序利用顺序表完成一个班级的一个学期的所有课程的管理:能够增加、删除、修改学生的成绩记录。

是博主在大二的时候写的,可能风格不太统一(还是太懒了)

C 复制代码
#include<stdio.h>
#include<stdlib.h>			//使用malloc得加入这个头文件
#include<string.h>                      //使用strcmp

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;

#define MAXLEN  100  
typedef struct node
{ 
	int num;    //学号
	char name[MAXLEN];  //姓名
	float score;     //成绩
	struct node *next;
}linklist,*LinkList;

void creat(linklist *&L)   //创建链表空白头节点
{
	L = (linklist *)malloc(sizeof(linklist));
	L->num=0;
	L->name[0]='\0';
	L->score=0;
	L->next=NULL;
}



void AddStu(linklist *&L)              //添加学生
{
	int x;
	linklist *s = NULL, *r = L;                    //s指向创建的结点  r是尾指针
	printf("请输入学生学号(输入0以结束):");
	scanf("%d",&x);
	while(x!=0)
	{
		s=(linklist *)malloc(sizeof(linklist));
		s->num=x;
		printf("请输入姓名:");    scanf("%s",s->name); 
		printf("请输入成绩:");  scanf("%f",&s->score); 
		r->next=s;
		r=s;
		printf("\n");
		printf("请输入学生学号(输入0以结束):");
		scanf("%d",&x);
	}
	r->next = NULL;
}

int listLength(LinkList L)				//不要头结点求表长
{
	int n = 0;
	LinkList p = L;					//定义遍历指针
	p = p->next;
	while (p != NULL)
	{
		n++;
		p=p->next;
	}
	return n;							//得到的长度
}


void SearchStu(LinkList L)                      //查找学生
{
	int c;
	if(L->next==NULL) printf("无学生信息\n");
	else
	{
		LinkList p;
		p=L->next;
		int id;
		char name1[MAXLEN];
		printf("按学号查找请输入1\t按姓名查找请输入2\t\n");
		scanf("%d",&c);
		if(c==1)
		{
			printf("请输入学生学号:");
			scanf("%d",&id);
			while((p->num!=id)&&(p!=NULL))
			{
				p=p->next;
			}
			if(p!=NULL) printf("学号:%d  姓名:%-15s  成绩:%5.1f\n",p->num,p->name,p->score);
			else printf("未查到该学生\n");
		}
		else
		{
			printf("请输入学生姓名:");
			scanf("%s",name1);
			while((strcmp(name1,p->name)!=0)&&(p!=NULL))

			{
				p=p->next;
			}
			if(p!=NULL) printf("学号:%d  姓名:%-15s  成绩:%5.1f\n",p->num,p->name,p->score);
			else printf("未查到该学生\n");
		}
	}
}

void StuInsert(LinkList L)           //插入学生
{
	LinkList  p,t;
	p=L;
	int index;
	int l;
	int i;
	printf("请输入插入的位置:");
	scanf("%d",&index);
	l=listLength(L);
	if(index>l+1) 
	{
		printf("插入位置超出表长且非表末端!\n");
		
	}
	else 
	{
		for(i=0;i<index-1;i++)     //找前驱结点
			p=p->next;
		t=(linklist *)malloc(sizeof(linklist));
		printf("请输入学号:"); scanf("%d",&t->num);
		printf("请输入姓名:"); scanf("%s",t->name);
		printf("请输入成绩:"); scanf("%f",&t->score);
		t->next=p->next;
		p->next=t;
	
	}
}
Status listDelete(linklist *&L)     //删除(学号)
{
	int del;
	printf("请输入要删除的学生的学号:");
	scanf("%d",&del);
	linklist *p1,*p2;        //p1指向前驱结点,p2指向删除结点
	p1=L;
	p2=L->next;
	while(p2!=NULL && p2->num!=del)
	{
		p1=p2;
		p2=p2->next;
	}
	if(p2==NULL) printf("未找到该生,删除失败!");
	else 
	{
		p1->next=p2->next;
		free(p2);
	}
	return OK;
}

void printList(linklist *L)       //输出链表
{

	LinkList p = L;
	p = p->next;					//因为头结点没有数据,所以移动到下一个结点
	printf("单链表显示:\n");
	if(p == NULL) printf("空表\n");
	while (p != NULL)
	{
		printf("学号:%d  姓名:%-15s  成绩:%5.1f\n",p->num,p->name,p->score);
		p = p->next;
	}
}
void Menu()             //文本菜单
{
	printf("                                                 *****************************\n");
	printf("                                                 * 学生成绩管理系统菜单      *\n");
	printf("                                                 *****************************\n");
	printf("                                                 +           1.查看学生      +\n");
	printf("                                                 +           2.添加学生      +\n");
	printf("                                                 +           3.插入学生      +\n");
	printf("                                                 +           4.查找学生      +\n");
	printf("                                                 +           5.删除学生      +\n");
	printf("                                                 +           6.显示菜单      +\n");
	printf("                                                 +           0.退出          +\n");
	printf("                                                 *****************************\n");
}
int main()
{
	int choose;
	LinkList L=NULL;
	creat(L);
	Menu();
	printf("请输入要执行功能的序号:");
	scanf("%d",&choose);
	while(choose!=0)
	{
		switch(choose)
		{
		case 1:printList(L);printf("\n");break;
		case 2:AddStu(L);printf("\n");break;
		case 3:StuInsert(L);printf("\n");break;
		case 4:SearchStu(L);printf("\n");break;
		case 5:listDelete(L);printf("\n");break;
		case 6:Menu();printf("\n");break;
		default: printf("输入错误!请重新输入!\n");break;
		}
		printf("请输入要执行功能的序号:");
		scanf("%d",&choose);
	}
	if(choose==0) printf("感谢使用!\n");
	return 0;
}

写在最后

在顺序表中做插入删除操作时,平均移动大约表中一半的元素,因此对n较大的顺序表效率低。 并且需要预先分配足够大的存储空间,估计过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。而链表恰恰是其中运用的精华。

博主实在是太懒了,循环链表也不高兴写了,下次想起来并且愿意的话再补吧(bushi