顺序表-单链表-循环链表

1 基本概念

1.1 数据

可以存储在计算机中,并且能被识别的(文字, 声音, 图像,视频)。

1.2 数据结构

数据逻辑关系(线性关系,非线性关系), 数据存储关系(顺序存储、链式存储), 算法。

1.3 算法

做某一件事情的过程(广义算法), 排序, 查找。

1.4 时间复杂度

时间复杂度一般指的是代码的语句执行总次数,称为语句频度。
image

1.5 空间复杂度

就是一段程序运行时所需的内存字节量。

2 线性表

对于一组拥有n个数据元素的线性表,其严格数学定义是:其中任何一个数据元素

\[a_i \]

,有且仅有一个直接前驱

\[a_{i-1} \]

,有且仅有一个直接后继

\[a_{i+1} \]

。首元素

\[a_0 \]

无直接前驱,尾元素

\[a_{n-1} \]

无直接后继。
满足这种数学关系的一组数据,当中的数据是一个挨着一个的,常被称为一对一关系。反之,如果数据之间的关系不是一对一的,就是非线性的。

数据定义必须一对一关系, 并且除了首尾元素, 所有元素有且只有直接前驱和后继。

如线性表的数据用 顺序存储,那么就是顺序表。
链式存储线性表就是链表。

3 顺序表SeqList

顺序存储就是将数据存储到一片连续的内存中,在C语言环境下,可以是具名的栈数组,或者是匿名的堆数组。

顺序表:数据必须是连续,一个数据接着一个数据。

比如用顺序存储 一组随机数10个。

顺序表删除和插入数据的时候要移动数据(移位)。

优点:连续空间-节省空间, 查询速度快。

缺点:插入和删除数据需要大量移位操作。

3.1 设计一个顺序表

struct  Sqlist{
      int length; //当前顺序表中数据个数
      int size; //当前顺序表容量  
      int *data; //申请堆空间数组
};

//设计操作顺序表的接口
struct Sqlist * createSq(int size);

bool destroySq(struct Sqlist* sq);

bool insertSq(struct Sqlist* sq, int pos, int data);//在指定位置pos插入数据

bool deleteSq(struct Sqlist* sq, int data);

void displaySq(struct Sqlist* sq);

3.2 细节⭐

  1. Sqlist里面的data也要malloc。
  2. 插入数据需要将数据从后面开始往后移,因为从前面往后移会覆盖原先的数据,出现无效移动。
  3. 删除时需要注意len需要-1。

4 单链表

链式存储线性表数据, 数据比较零散存储, 在每一个数据中包含一个指针域(指向下一个或者上一个数据)。

4.1 设计单链表结构体

struct Node{
      int data;  //数据域
      struct Node *next ;//指针域
};

4.2 链表接口设计

//创建节点--链表
struct  Node* create_node(int data);

//销毁链表
bool destroy_node(struct Node* head);

//插入节点
bool insert_node_head(struct Node* head,  struct Node* newNode);//头插法
bool insert_node_tail(struct Node* head, struct Node* newNode);//尾插法

//删除节点
bool delete_node(struct Node *head, int data);

//遍历节点
void display_node(struct Node* head);

4.3 细节⭐

  1. malloc是⭐#include <stdlib.h>头文件里面的。
  2. 在单链表中,⭐head 通常是一个哨兵节点,它并不存储数据,而是指向链表的第一个数据节点。因此,head 的 next 指针指向实际的数据节点。
  3. 在main函数里面需要先malloc哨兵节点head,然后再创建节点来插入节点。
  4. 销毁单链表时,需要
    1. 在销毁节点前保存下一个节点的Node*值
    2. 然后销毁
    3. 最后移动节点至下一个节点(也就是销毁前保存好的节点)
  5. 头插入链表时需要注意要插在head哨兵节点后面,head下一个节点的前面。
  6. 删除节点需要
    1. 删除节点前保存要删除的节点
    2. 删除节点后(即将前节点的next指向要删除节点的next)
    3. 销毁要删除的节点(之前保存好的)
  7. 遍历节点从head->next开始。

4.4 作业

4.4.1 翻转链表

用链表存储一组数据比如1,2,3,4,5,请实现链表倒序变为5,4,3,2,1.

4.4.1.1 法一(老师)

额外使用了新的空间

image

image

image

代码:

#include <stdio.h>
#include <stdlib.h>

struct Node{
    int data;
    struct Node* next;
};

struct Node* create_node(int data){

    struct Node* newNode = malloc(sizeof(struct Node));
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

//显示链表元素
void  display_node(struct Node *head){
    if(head == NULL) return ;
    head = head->next;
    while(head != NULL){
        printf("%d ", head->data);
        head = head->next;
    }printf("\n");
}

int main(void){
    struct Node *head = create_node(0);

    for(int i=1; i<=5; i++){
        struct Node *newNode = create_node(i);

        struct Node *p = head;
        while(p->next != NULL) p = p->next; //把p移动到链表末尾

        p->next = newNode;
    }
    display_node(head);

    //倒序
    struct Node* thead = create_node(0);

    while(1){
        //把head后面的节点脱离链表
        struct Node* p = head->next;
        if(p==NULL) 
        {
            //将thead的链表挪到head后面,并销毁thead
            head->next = thead->next;
            free(thead);
            break;
        }
        
        //更改p节点位置前,保存p->next,用于下一次循环
        head->next = p->next;

        //把p节点接入thead后面
        //使用头插法--将p节点头插入thead链表
        p->next = thead->next;
        thead->next = p;
    }
    display_node(head);
    //display_node(thead);
}

4.4.1.2 法二(自己写的)

image

(自己写的代码)

sLList.c

void reversal_node(Node **head)
{
	Node *p = (*head)->next;		//⭐⭐*head需要加括号
	Node *r = p->next;				//先保留p的next,因为p->next需要指向NULL,表示末尾
	p->next = NULL;
	Node *temp = NULL;				//用于循环时使用
	while(r != NULL)				//我们可以看到规律:各个表达式首尾相连
	{
		temp = r->next;				//先存储r->next,表示后一个数据
		r->next = p;				//将r->next指向前一个数据
		p = r;						//p往前挪,仍表示前一个数据
		r = temp;					//r往前挪,表示当前需要修改指针的数据
	}
	(*head)->next = p;				//修改head->next的指向
}

main.c

	Node *head = (Node *)malloc(sizeof(Node)); // 创建
	head->next = NULL;

	int len = 0;
	printf("请输入要添加数据的个数:");
	scanf("%d", &len);
	for (int i = 0; i < len; i++)
	{
		insert_node_head(head, create_node(random() % 100)); // 插入
	}
	display_node(head); // 显示

	printf("------翻转------\n");
	reversal_node(&(head));//⭐传入地址,来改变main的head值,而不是只更改翻转函数内部head值
	display_node(head);

	if (destroy_node(head)) // 销毁
		printf("成功销毁\n");

4.4.2 交换节点

image

sLList.c

// 自己写的交换节点代码
// 交换data这个节点和data的下一个节点
bool swap_node_data(Node **head, int data)
{
	Node *p = (*head);
	while (p != NULL && p->next != NULL)
	{
		if(p->next->next==NULL) return false;
		if (p->next->data == data)
		{
			swap_node(&p);
			return true;
		}
		p = p->next;
	}
	return false;
}

//自己写的
//交换节点的核心代码
//交换的是p->next和p->next->next
bool swap_node(Node **p)
{
	Node *r = (*p)->next;		//⭐⭐更改后续指针之前保存后续指针的值
	Node *q = r->next;
	r->next = q->next;
	q->next = r;
	(*p)->next=q;
	return true;
}

main.c

	Node *head = (Node *)malloc(sizeof(Node)); // 创建
	head->next = NULL;

	int len = 0;
	printf("请输入要添加数据的个数:");
	scanf("%d", &len);
	for (int i = 0; i < len; i++)
	{
		insert_node_head(head, create_node(random() % 100)); // 插入
	}
	display_node(head); // 显示

	printf("请输入要交换的位置的第一个数据:");
	int swap_data = 0;
	scanf("%d", &swap_data);

	printf("------交换节点------\n");
	swap_node_data(&(head), swap_data);//⭐传入地址
	display_node(head);

	if (destroy_node(head)) // 销毁
		printf("成功销毁\n");

4.4.3 冒泡排序

  1. 使用了交换节点的函数。
  2. 注意,p从head开始
    1. 因为要比较的数据是p->next->data和p->next->next->data。
    2. 这样方便保存要交换的位置的前一个节点。

sLList.c

// 冒泡排序
bool sort_node(struct Node *head)
{
	bool flag = false;
	if(head==NULL) return false;

	do
	{
		flag = false;
		Node *p = head;
		if(p==NULL) return true;
		while (p->next != NULL&&p->next->next!=NULL)
		{
			if (p->next->data > p->next->next->data)
			{ 
			// 只交换节点的数据
/*				int temp = p->data;
				p->data = p->next->data;
				p->next->data = temp;
*/
				swap_node(&p);
				flag = true;
				// display_node(head);
			}
			p = p->next;
		}
	} while (flag);
}

main.c

	Node *head = (Node *)malloc(sizeof(Node)); // 创建
	head->next = NULL;

	int len = 0;
	printf("请输入要添加数据的个数:");
	scanf("%d", &len);
	for (int i = 0; i < len; i++)
	{
		insert_node_head(head, create_node(random() % 100)); // 插入
	}
	display_node(head); // 显示

	printf("------排序------\n");
	sort_node(head);
	display_node(head);

	if (destroy_node(head)) // 销毁
		printf("成功销毁\n");

5 单向循环链表

5.1 设计一个链表结构体

struct CNode{
    int data;
    struct CNode* next;
};

5.2 创建节点函数

struct CNode* create_cnode(int data){
    struct CNode* node = malloc(sizeof(struct CNode));
    node->data = data;
    node->next = node;
}

5.3 插入节点

//头插法
bool  insert_cnode_head(struct CNode* head, struct CNode* newNode)
{
    if(head == NULL || newNode == NULL) return false;
    newNode->next = head->next;
    head->next = newNode;
}
//尾插法
bool insert_cnode_tail(struct CNode* head, struct CNode* newNode)
{
    //找到尾
    struct CNode* p = head;
    while(p->next != head) p = p->next;
    insert_cnode_head(p, newNode);
}

5.4 删除节点

struct CNode* delete_cnode(struct CNode*head, int data)
{
    if(head == NULL) return NULL;
    if(head->data == data)  //头是要删除的数据
    {
        struct CNode* p = head->next;
        while(p->next != head) p = p->next;  
        p->next = head->next;
        free(head); 
        return p->next;
    }
    struct CNode *p = head;
    while(p->next != head){
        if(p->next->data == data){
            struct CNode* t = p->next;  
            p->next = t->next;
            free(t);
            return head;  
        }
        p = p->next;
    }
    return head;
}

5.5 作业

//注意:以下为核心代码,非全部代码

5.5.1 单向循环链表应用

1.设计一个链表存储1-15自然数 ,实现从头开始1,2,3第三个节点删除,后面继续数1,2,3,删除数到3所在的节点以此类推,最后剩下的一个节点输出

main.c

CNode *head = create_cnode(1);
for (int i = 15; i > 1; i--)
{
	insert_cnode_head(head, create_cnode(i));
}
display_cnode(head);

CNode *p = head;
int flag = 0;
while (p->next != p)
{
	flag++;
	if (flag == 2)
	{
		printf("delete:%d,\t", p->next->data);
		head = delete_cnode_by_pointer(head, p);
		display_cnode(head);
		flag = 0;
	}
	p = p->next;
}
printf("result:\t\t");
display_cnode(head);

/*
	printf("请输入你要删除的数据:");
	int del = 0;
	scanf("%d", &del);
	head = delete_cnode_by_data(head, del);
	display_cnode(head);
*/

CLinkedList.c

#include "CLinkedList.h"

//创建
struct CNode* create_cnode(int data)
{
	CNode *n=(CNode*)malloc(sizeof(CNode));
	if(n==NULL) return n;
	n->data=data;
	n->next=n;
	return n;
}

//插入
//头插
bool  insert_cnode_head(struct CNode* head, struct CNode* newNode)
{
	if(head==NULL||newNode==NULL) return false;
	newNode->next=head->next;
	head->next=newNode;
	return true;
}

//尾插
bool insert_cnode_tail(struct CNode* head, struct CNode* newNode);

//删除节点
struct CNode* delete_cnode_by_data(struct CNode*head, int data)
{
	CNode *p=head;
	if(p==NULL) return head;
	//如果要删除头节点
	else if(head->data==data)
	{
		while(p!=NULL&&p->next!=head) p=p->next;
		p->next=head->next;
		free(head);
		return p->next;
	}
	//如果删除的不是头节点
	else
	{
		while(p->next!=head)
		{
			if(p->next->data==data)
			{
				CNode *r=p->next;
				p->next=p->next->next;
				free(r);
				return head;
			}
			else p=p->next;
		}
	}
}

//删除p->next节点
struct CNode* delete_cnode_by_pointer(struct CNode*head,struct CNode*p)
{
	if(head==NULL) return head;
	//如果要删除头节点
	else if(p->next==head)
	{
		p->next=head->next;
		free(head);
		return p->next;
	}
	//如果删除的不是头节点
	else
	{	
		CNode *r=p->next;
		p->next=p->next->next;
		free(r);
		return head;
	}
}

//遍历
void display_cnode(CNode* head)
{
	if(head==NULL) return;
	printf("display:");
	CNode *p=head;
	do
	{
		printf("%d ",p->data);
		p=p->next;
	}while(p!=head);
	printf("\n");
}

5.5.2 判断有没有环

检测一个单链表是否存在环(思考---面试问题)

main.c

// 快慢速度问题

srand(time(NULL));

// 有环链表
CNode *head_circle = (CNode *)malloc(sizeof(CNode));
head_circle->data = 1;
head_circle->next = NULL;
create_circle(head_circle);
// display_circle(head_circle);

// 无环链表
CNode *head_list = (CNode *)malloc(sizeof(CNode));
head_list->data = 1;
head_list->next = NULL;
create_list(head_list);
//display_list(head_list);

if(judge_circle(head_list)==false) printf("head_list是无环链表\n");
else printf("head_list是有环链表\n");

if(judge_circle(head_circle)==false) printf("head_circle是无环链表\n");
else printf("head_circle是有环链表\n");

CLinkedList.c

//---------------------------------------------------有环链表代码

//创建链表节点
static CNode* create_node(int data)
{
	CNode *n=(CNode*)malloc(sizeof(CNode));
	n->data=data;
	n->next=NULL;
	return n;
}

//插入节点
static void insert_node(CNode* head,CNode* p)
{
	p->next=head->next;
	head->next=p;
}

//创建有环链表
void create_circle(CNode* head)
{
	for(int i=6;i>1;i--)
	{
		insert_node(head,create_node(i));
	}
	CNode *end=head,*p=head;
	while(end->next!=NULL) 
	{
		if(p->data<3) p=p->next;
		end=end->next;
	}
	end->next= p;
}

//检测有环链表,输出元素检查
void display_circle(CNode* head)
{
	CNode *p=head;
	for(int i=0;i<15;i++)
	{
		printf("%d ",p->data);
		p=p->next;
	}
	printf("\n");
}

//---------------------------------------------无环链表代码
//创建无环链表
void create_list(CNode* head)
{
	for(int i=6;i>1;i--)
	{
		insert_node(head,create_node(i));
	}
}

//检测无环链表,输出元素检查
void display_list(CNode* head)
{
	CNode *p=head;
	while(p!=NULL)
	{
		printf("%d ",p->data);
		p=p->next;
	}
	printf("\n");
}

//-----------------------------------------判断有无环
//有环就返回true,否则返回false
bool judge_circle(CNode* head)
{
	CNode* fast=head,*slow=head;
	int flag=0;
	while(fast!=NULL&&slow!=NULL)
	{
		flag++;
		//偶数的时候slow再往后娜
		if(flag%2==0) slow=slow->next;
		fast=fast->next;
		if(slow==fast) return true;
	}
	return false;
}

6 双向循环链表

6.1 定义

每个节点中包含一个前驱指针, 和一个后继指针。

双向链表结构体如下:

struct  Dnode {
    struct Dnode* prev;//指向前驱
    struct Dnode* next;//指向后继
    int data; //数据
}

6.2 创建节点

//创建节点
Dnode *create_dnode(int data)
{
	Dnode *n=(Dnode*)malloc(sizeof(Dnode));
	n->data=data;
	n->prev=n;
	n->next=n;
	return n;
}

6.3 插入节点

6.3.1 头插法

6.3.1.1 只有头结点时插入节点

image

6.3.1.2 有2个或以上的结点时插入节点

image

6.3.1.3 代码

//头插法
bool insert_dnode_head(Dnode* head,Dnode* p)
{
	if(head==NULL||p==NULL) return false;
	//只有一个头节点时
	if(head->next==head)
	{
		p->next=head->next;
		p->prev=head->prev;
		head->next=p;
		head->prev=p;
		return true;
	}
	else
	{
		p->next=head->next;
		p->prev=head;
		head->next->prev=p;
		head->next=p;
		return true;
	}
}

6.3.2 尾插法

只有头节点时插入的原理与头插法一致。

有2个或以上的节点时插入节点:

image

代码:

//尾插法
bool insert_dnode_tail(Dnode* head,Dnode* p)
{
	if(head==NULL||p==NULL) return false;
	//只有一个头节点时
	if(head->next==head)
	{
		p->next=head->next;
		p->prev=head->prev;
		head->next=p;
		head->prev=p;
		return true;
	}
	else
	{
		p->next=head;
		p->prev=head->prev;
		head->prev->next=p;
		head->prev=p;
		return true;
	}
}

6.4 删除节点

6.4.1 若删除头节点

需要注意此时是只有一个头节点还是不止一个头节点,需要分情况讨论。

  1. 如果不止一个头节点:

image

  1. 如果只剩下一个头节点,则只需要free(head);

6.4.2 删除非头节点

image

6.4.3 代码

//删除节点
Dnode* delete_dnode(Dnode* head,int data)
{
	if(head==NULL) return NULL;
	Dnode *p=head;
	
	//如果删除的是头节点且此时只有一个节点
	if(p->data==data&&p->next==p)
	{
		free(head);
		return NULL;
	}
	//如果删除的是头节点且此时不止一个节点
	if(p->data==data&&p->next!=p)
	{
		Dnode *tail=head->prev;//最后一个节点
		tail->next=head->next;
		head->next->prev=tail;
		free(head);
		return tail->next;
	}
	//如果删除的不是头节点
	else
	{
		p=p->next;
		while(p!=head)
		{
			if(p->data==data)
			{
				p->prev->next=p->next;
				p->next->prev=p->prev;
				free(p);
				return head;
			}
			else p=p->next;
		}
	}
}
posted @ 2025-05-13 16:37  cyt2730  阅读(69)  评论(0)    收藏  举报