算法通关村第一关-链表青铜挑战笔记

1、链表的概念

存储数据的一种结构,不强制在内存中集中存储,元素分散存储。一个元素配备一个指针,指针指向后继元素。

定义:数据元素随机存储在内存中,通过指针维系数据之间“一对一”的逻辑关系。

核心:一个节点只能有一个后继节点。但是不代表一个节点只能有一个被指向。

不正确:

image.png

正确: 

image.png

重要概念:节点和头结点,虚拟节点。 

节点:由值和指向下一个节点的地址组成的独立单元。

头结点:单链表的第一个节点。可以进行链表的遍历。

虚拟节点:虚拟节点的指针指向head节点,val值不会被使用,可以被初始化为0或者-1。方便我们处理首节点,可用于链表的反转等。

2、链表的构造

首先我们要想进行构造链表,必须要明白链表的组成?正如我前面讲到的,链表是由节点组成的,那节点又是有什么组成的呢?

image.png

 见上图,一个节点是由数据+指针组成的,数据就代表当前节点所存放的数据,而指针就指向下一个节点的指针。由此我们就可以得到链表的数据结构图:

image.png

创建节点:

// C语言
struct ListNode {
    int val;    //代表数据
    struct ListNode *next;    //代表指针
}

创建函数:

struct ListNode* initLink() 
{
    int i;
    //1、创建头指针
    struct ListNode* p = NULL;
    //2、创建头结点
    struct ListNode* temp = (struct ListNode*)malloc(sizeof(struct ListNode));
    temp->val = 0;
    temp->next = NULL;
    //头指针指向头结点
    p = temp;
    //3、每创建一个结点,都令其直接前驱结点的指针指向它
    for (i = 1; i < 5; i++) 
    {
        //创建一个结点
        struct ListNode* a = (struct ListNode*)malloc(sizeof(struct ListNode));
        a->val = i;
        a->next = NULL;
        //每次 temp 指向的结点就是 a 的直接前驱结点
        temp->next = a;
        //temp指向下一个结点(也就是a),为下次添加结点做准备
        temp = temp->next;
    }
    return p;
}

测试方法:

int main() 
{
    struct ListNode* p = NULL;
    printf("初始化链表为:\n");
    //创建链表{1,2,3,4}
    p = initLink();
    return 0;
}

 3、链表的增删改查

不管对于什么数据结构,只要有数据的存储就一定会有数据的取出,而不管我们使用什么数据结构还是算法,对该数据结构的遍历、新增、删除都是至关重要的!!!

3.1链表的遍历

image.png

思路:利用链表的存储方式,以及定义来进行遍历。找到当前链表的头结点!!!

//打印链表
void printList(struct ListNode* p) {
    struct ListNode* temp = p;//temp指针用来遍历链表
    //只要temp指向结点的next值不是NULL,就执行输出语句。
    while (temp) {
        // struct ListNode* f = temp;//准备释放链表中的结点
        printf("%d ", temp->val);
        temp = temp->next;
        // free(f);
    }
    printf("\n");
}

//获取链表的长度
int32_t getLength(struct ListNode* p) {
    struct ListNode* temp = p;//temp指针用来遍历链表
    int length=0;
    //只要temp指向结点的next值不是NULL,就执行输出语句。
    while (temp) {
        // struct ListNode* f = temp;//准备释放链表中的结点
        length++;
        temp = temp->next;
        // free(f);
    }
    return length;
}

测试方法:

int main() {
    struct ListNode* p = NULL;
    printf("create list: \t\n");
    //创建链表{1,2,3,4}
    p = initLink();
    printList(p);
    int length=getLength(p);
    printf("list length: %d\n",length);
    return 0;
}
3.2链表的插入

思路:单链表的插入和数组结构方式的插入大同小异。我们要明白插入的具体逻辑,只有逻辑清楚了,才能进行实施。

类型:头插法、中间插、尾插法。

方法:前面讲过链表的插入和数组差不多,那我们不妨回想一下数组元素是怎么插入的。假设我们数组存放的类型是整数型,那么它就是由很多整数组成的。那么我们首先要创造一个我们想要插入的整数,之后再找到我们想要插入的具体位置,在我们找到插入的位置后,我们需要将当前位置的数据保存下来,把我们插入的数据放进去。这样基本就完成了数组元素的插入。

接下来我们再回到正题!!是不是就很简单了。

既然链表是由节点组成的,那么我们进行链表插入的时候自然是先创建一个新的节点new,之后再利用链表的遍历找到我们想要插入的位置curr。那么我们想要插入到当前位置难么我们要创建一个临时节点temp指向当前插入的节点。此时的关系就是pre(curr的前驱结点)指向curr,temp指向curr,此时我们还需要做的就是将pre指向new,new指向temp的指向

头插法:

image.png

 中间插:

image.png

 尾插法:

image.png

 代码:

struct ListNode* insertNode(struct ListNode* head, struct ListNode* nodeInsert, int position) {
    if (head == NULL) {
        // 这里可以认为待插入的节点就是链表的头节点,也可以抛出不能插入的异常
        return nodeInsert;
    }
    int size = getLength(head);
    if (position > size + 1 || position < 1) {
        printf("位置参数越界");
        return head;
    }

    // 插入节点到头部
    if (position == 1) {
        nodeInsert->next = head;
        head = nodeInsert;
        return head;
    }

    struct ListNode* pNode = head;
    int count = 1;
    // 遍历链表,找到插入位置的前一个节点
    while (count < position - 1) {
        pNode = pNode->next;
        count++;
    }
    nodeInsert->next = pNode->next;
    pNode->next = nodeInsert;

    return head;
}

void testInsert(){

   struct ListNode* head = NULL;
   struct ListNode* node = (struct ListNode*)malloc(sizeof(struct ListNode));
   node->val=1;
//    插入第一个元素
     head=insertNode(head,node,1);
   printList(head);
 
 //    插入第二个元素,因为前面至于一个元素,这里就是在尾部插入了
    node = (struct ListNode*)malloc(sizeof(struct ListNode));
    node->val=3;
    head=insertNode(head,node,2);
    printList(head);


 //    插入第二个元素,后面有个三,所以这里就是中间位置插入了
    node = (struct ListNode*)malloc(sizeof(struct ListNode));
    node->val=2;
    head=insertNode(head,node,2);
    printList(head);
}
3.3链表的删除

类型:头删除、中间删、尾删除。

方法:前面讲过链表的删除和数组差不多,那我们不妨回想一下数组元素是怎么删除的。我们首先要根据我们想要删除的数找到数组中对应的位置,保存当前位置下一个位置的状态,将当前位置的前驱位置指向当前位置的下一个位置。

接下来我们再回到正题!!是不是就很简单了。

首先根据遍历,找到我们想要删除的节点的位置curr。将当前位置的前驱节点pre指向当前位置的next。

头删除:

image.png

 中间删除:

image.png

 尾删除:

image.png

 代码:

struct ListNode* deleteNode(struct ListNode*head, int position) {
    if (head == NULL) {
        return NULL;
    }
    int size = getLength(head);
    if (position > size || position < 1) {
        printf("输入的参数有误\n");
        return head;
    }
    if (position == 1) {
        struct ListNode*curNode=head;
       head= head->next;
        free(curNode);
        return head;
    } else {
        struct ListNode* cur = head;
        int count = 1;
        while (count < position - 1) {
            cur = cur->next;
            count++;
        }
        struct ListNode*tmp = cur->next;
        cur->next = tmp->next;
        free(tmp);
        return head;
    }
}

void testDelete(){

    struct ListNode* p = NULL;
    printf("create list: \t\n");
    //创建链表0~9
    p = initLink();
    printList(p);

    // 删除第一个元素0
   p= deleteNode(p,1);
    printList(p);

   //删除中间元素
       p= deleteNode(p,5);
    printList(p);

   //删除末尾元素9
       p= deleteNode(p,8);
    printList(p);
  
}

扩展:

如果链表是有序的,如何根据顺序进行链表的插入和删除

和链表的插入和删除一样,正常链表的插入和删除,是遍历到想要插入或者想要删除的位置,之后再进行删除或者插入。有序的话还是同样的进行遍历,如果当前节点的值是小于(大于)插入的值而下一个节点的值大于插入的值就进行插入。

代码:

//添加节点
void AddStruct(struct NUM **head,int ipnum)
{
	struct NUM *new; 	//保存新节点地址
	struct NUM *temp;	//当前地址
	struct NUM *last;	//上一节点地址
	
	last = NULL;
 
	new = (struct NUM *)malloc(sizeof(struct NUM));//创建节点
	if(new == NULL)
	{
		printf("内存分配失败\n");
		exit(1);
	}	
 
	//printf("内存分配成功\n");
	new->num = ipnum; 	 //保存数据到新节点
	
	if(*head != NULL)//不是空链表
	{
		
		temp = *head; //传入的是指针的地址即指针的指针,取值即可得到指针
		//printf("不是空链表\n");
		while(temp != NULL && ipnum > temp->num) //指针没有指到链表结尾且插入值大于当前节点值
		{
			last = temp;			//保存插入位置前一节点地址,该节点的指针指向新节点
			temp = temp->next;		//指针往后走
		}
 
		if(ipnum <= (*head)->num) //如果输入的数值小于或等于第一个节点的值
		{
			new->next = *head;	//新节点的指针指向插入前的第一个节点
			*head = new;		//头指针指向新节点 (相当于头插法)
		}
		else
		{
			last->next = new;		//插入位置的前一节点的指针指向新节点
			new->next = temp;		//新节点的指针指向后一节点
		}	
	}
	else //是空链表
	{
		printf("是空链表\n");
		*head = new;
		new->next = NULL;
	}
}

/*删除节点*/
void DeleteStruct(struct NUM **head,int ipnum)
{
	struct NUM *temp;
	struct NUM *last;
 
	temp = *head; 
	last = NULL;
	while(temp != NULL && ipnum != temp->num)  //指针没有指到链表结尾且插入值不等于当前节点值
	{
		last = temp;			//保存插入位置前一节点地址,该节点的指针指向新节点
		temp = temp->next;		//指针往后走
	}
	
	if(temp == NULL)
	{
		printf("找不到该节点!\n");
	}
	else
	{
		if(last == NULL) //该值在第一个节点(上面的while不执行)
		{
			*head = temp->next; //头指针指向后一节点
		}
		else
		{
			last->next = temp->next;	//上一节点的指针指向后一节点
		}
		free(temp);  //释放空间
		printf("已删除该节点!\n");
		
	}
 
}

双向链表的构造?双向链表插入和删除?

双向链表和单链表一样都是由节点组成的,而单链表的节点包括当前节点的值和指向下一节点的指针。双向链表比单链表多的唯一的就是前驱指针。它指向的是前一个节点。同时形成闭环

image-20221022145010877

哨兵位节点双向链表 在只有一个哨兵位时,让它自己指向自己。哨兵位的 next 指向它自己的prev ,哨兵位的 prev 指向它自己的 next 。说白了就是一个特殊的环形链表。

构造:

typedef struct ListNode
{
	LTDataType data; // 保存数据
	struct ListNode* next; // 记录下一个节点的地址
	struct ListNode* prev; // 记录上一个节点的地址
}LTNode;

尾插:image-20221022155719811

 头插:image-20221022160633145

 尾删:image-20221022162006011

头删: image-20221022170755020

// 初始化
LTNode* ListInit()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	// 双向带头循环链表的prev指向next,next指向prev
	// 但是这里只有一个节点,所以只能让它自己指向自己
	if (phead == NULL)
	{
		perror("ListInit");
		exit(-1);
	}
	phead->next = phead;
	phead->prev = phead;
	
	return phead;
}

// 创建新节点
LTNode* BuyListNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("ListPushBack");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

// 尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);// 一定不为空-->有哨兵位

	LTNode* tail = phead->prev;// 尾就是prev,由于是双向循环链表,所以头的prev就是尾
	LTNode* newnode = BuyListNode(x);

	// phead              tail            newnode
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

// 头插
void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyListNode(x);
	LTNode* next = phead->next;
	
	phead->next = newnode;
	newnode->prev = phead;
	
	newnode->next = next;
	next->prev = newnode;
}

// 尾删
void ListPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);// 防止把哨兵位删掉 
	LTNode* tail = phead->prev; 
	LTNode* tailprev = tail->prev;
	free(tail);
	
	phead->prev = tailprev;
	tailprev->next = phead;
}
//头删
void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
    
	LTNode* next = phead->next;
	LTNode* nextNext = next->next;

	phead->next = nextNext;
	nextNext->prev = phead;
	free(next);
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值