부지런하고 아무도 기다리지 않는 세월을 격려하십시오
C/C++ 게임 개발
안녕하세요 미나님 Jun Xi_입니다 오늘은 지난번 싱글링크드리스트에 이어 대표적인 양방향 순환링크리스트에 대해 말씀드리겠습니다 이런 종류의 학습 연결 목록은 매우 중요합니다. 이해하기 쉬운 단어와 논리 다이어그램을 사용하여 이해를 돕기 위해 최선을 다할 것입니다
. —
이중 연결 리스트의 선두주자
- 다음은 주요 양방향 순환 연결 목록의 논리도입니다.
1. 단일 연결 리스트의 특성과 다름
- 1. 양방향: 양방향은 선행 양방향 순환 연결 목록의 구조에서 연결 목록을 연결하는 두 개의 포인터가 있음을 의미합니다. 그 중 하나는 이전 노드를 가리키고 다른 포인터는 다음 노드를 가리킵니다. 다음 노드.
- 2. 루프: 단일 연결 리스트의 테일 노드는 NULL을 가리키고 양방향 순환 연결 리스트의 테일 노드는 헤드 노드 헤드를 가리키고 헤드의 이전 노드에 대한 포인터는 테일 노드를 가리킨다(I 볼 그림과 결합 이해하지 못함)
- 3. Take the lead: take the lead는 링크드 리스트의 헤드로서 양방향 순환 링크드 리스트에 센티넬 비트가 있다는 것을 의미합니다. 아래에 집중)
2. 노드에 저장된 요소
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;//链表中存的值
struct ListNode* next;//指向下一个结点的指针
struct ListNode* prev;//指向上一个结点的指针
}ListNode;
3. 연결 리스트 기능 BuyListNode 초기화
//初始化双链表,创建节点
ListNode* BuyListNode(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));//malooc开辟一个结点的空间
if (newnode == NULL)//判断一下是否开辟空间成功
{
perror("malloc failed\n");
exit(-1);
}
//初始化时把两个指针都置空
newnode->next = NULL;
newnode->prev = NULL;
newnode->data = x;
return newnode;//返回创建好的链表节点
}
- 이 단계는 비교적 간단합니다. 주석을 보고 코드가 매우 이해하기 쉽다고 생각하므로 너무 많은 설명이 필요하지 않습니다.
4. 연결 리스트의 헤드 노드 ListCreate 생성
// 创建返回链表的头结点.
ListNode* ListCreate()
{
ListNode* phead = BuyListNode(0);//开辟结点所需动态空间
phead->next = phead;
phead->prev = phead;
return phead;
}
- 연결 리스트의 헤드에 센티널 비트가 있다고 소개했는데, 센트리 비트가 연결 리스트에 노드가 없을 때 헤드 포인터와 테일 포인터가 모두 자신을 가리키고 있는데 센티널 비트의 의미는 무엇입니까?
센티넬 비트란 무엇이며 센티넬 비트의 존재 의미
- 센티넬 비트는 알고리즘 구현을 단순화하고 효율성을 높이기 위해 데이터 구조에 사용되는 특수 값 또는 노드입니다. 검색 작업에서 센티넬 위치는 경계를 넘어 찾을 수 없는 상황을 피하기 위해 적절한 위치에 미리 설정할 수 있습니다. 예를 들어 연결된 목록에서 센티넬 비트는 연결된 목록의 위치를 찾는 데 사용되며 빈 연결 목록에도 센티넬 비트가 있습니다. 삽입 정렬에서 센티넬 비트는 이동 데이터의 순환 처리를 지원하는 데 사용되며 이동해야 하는 데이터를 저장할 수 있으며 후속 작업에서 덮어쓰지 않습니다. 센티넬 비트를 사용함으로써 알고리즘의 구현을 단순화할 수 있고, 경계 판단 횟수를 줄일 수 있으며, 프로그램의 실행 효율을 높일 수 있다.
- 구체적으로 말하면 연결 목록의 의미는 무엇입니까?
- Linked List를 사용할 때 Sentinel Bit가 없으면 Out-of-bound 접근과 빈 Linked List를 방지하기 위해 Linked List를 사용할 때마다 Null 연산을 수행해야 하므로 효율성에 큰 영향을 미칩니다. 프로그램 운영의. 그리고 이 센티널 비트가 있을 때, 이것이 빈 연결 리스트일 때, 이때의 phead는 자신을 가리키는 노드이므로 오류가 발생하지 않을 것이고, 연결 리스트에 노드가 있을 때 그것은 자신을 가리키는 노드이다. 헤드 노드에 루프에도 편리합니다. 테일 노드의 다음 노드를 이 phead로 직접 가리킬 수 있습니다.
5. 연결 리스트를 출력하는 ListPrint 함수
// 双向链表打印
void ListPrint(ListNode* phead)
{
assert(phead);//断言,判断是否有结点,
printf("Phead");//没有其他结点,打印phead
ListNode* cur = phead->next;
while (cur!= phead)
{
printf(" <==> %d", cur->data);
cur = cur->next;
}
printf("\n");
}
- 기본적으로 이전 단일 연결 리스트의 출력과 동일하지만 우리는 순환 연결 리스트이기 때문에 연결 리스트의 꼬리는 더 이상 NULL을 가리키지 않습니다. 는 phead를 가리키므로 연결 리스트를 순회할 때 연결 리스트가 다시 phead와 같을 때 연결 리스트가 한 번 순회했음을 의미하며 순회할 때 루프가 중지되는 조건입니다.
6. 연결 리스트를 파괴하는 ListDestory 함수
// 双向链表销毁
void ListDestory(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
while (cur!=phead)
{
ListNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
- 연결 리스트 전체를 순회하고 각 노드를 해제하며, 연결 리스트를 순회하는 루프 조건은 위의 연결 리스트 출력과 동일합니다.
- 자, 위의 기본 인터페이스에 대해 이야기한 후 다음 단계는 하이라이트입니다. 연결된 목록에 노드를 삽입하는 방법은 무엇입니까?
7. Linked List의 헤드 삽입 및 헤드 삭제
- 사실 삽입과 삭제에 관해서는 이전 단일 연결 리스트의 삽입과 삭제를 이해할 수 있다면 이 작품의 논리는 사실 이해하기 매우 쉽습니다.
플러그
// 双向链表头插
void ListPushFront(ListNode* phead, LTDataType x)
{
assert(phead);//断言,
ListNode* frist = BuyListNode(x);//创建节点
ListNode* cur = phead->next;
frist->next = cur;
frist->prev = phead;
cur->prev = frist;
phead->next = frist;
}
- 여기서 우리가 이루고자 하는 목표를 사진을 통해 먼저 살펴보자
首先,我们需要保存一下第一个位置的地址,也就是图中的cur,然后我们让first的next指向此时的cur,first的prev指向头head,这样就建立起了结点间的联系
但是由于我们需要链表是一个双头链表,我们此时还需要让head的next指向插入的first,cur的prev也指向first这样才算真正完成我们的头插
헤드 삭제
// 双向链表头删
void ListPopFront(ListNode* phead)
{
assert(phead);
ListNode* frist = phead->next;
phead->next = frist->next;
frist->next->prev = phead;
free(frist);
}
- phead의 next가 first의 다음 노드를 가리키도록 하고, first의 다음 노드의 prev가 phead를 가리키도록 하고 이때 free를 해제하고 head를 삭제합니다.
- 헤드플러그와 유사하게 위의 논리도를 거꾸로 보면 되므로 도면을 그리느라 공간을 낭비하지 않아도 됩니다.
8. Linked List 테일 삽입 및 삭제
테일 플러그
// 双向链表尾插
void ListPushBack(ListNode* phead, LTDataType x)
{
assert(phead);
assert(phead->prev);
ListNode* newnode = BuyListNode(x);
//ListInsert(phead, x);
ListNode* tail = phead->prev;//起初的尾
newnode->next = phead;//插入的结点指向头
tail->next = newnode;//之前的尾指向现在的尾
newnode->prev = tail;
tail = newnode;
}
不同于单链表的是,由于我们的链表是循环链表,其实尾部的结点就保存在phead的prev中,因此不再需要遍历就能轻易的找到我们的尾部
此时,就比较简单啦,我们让现在的尾(d3)的next指向插入的newnode,让newnode的prev指向d3,next指向phead,再让phead的prev指向插入的newnode,即可完成尾插
꼬리 삭제
// 双向链表尾删
void ListPopBack(ListNode* phead)
{
assert(phead);
assert(phead->prev);
ListNode* tail = phead->prev;
ListNode* tailprev = phead->prev->prev;
tailprev->next = phead;
tail = tailprev;
free(phead->prev);
// ListErase(phead->prev);
}
- 꼬리를 삭제하려면 현재 꼬리의 이전 노드를 찾아 현재 꼬리로 바꾸고 마지막으로 현재 꼬리를 해제해야 합니다.
9. 연결된 목록에 값이 있는지 확인하는 ListFind 함수
// 双向链表查找
ListNode* ListFind(ListNode* phead, LTDataType x)
{
assert(phead);
ListNode* cur = phead;
while (cur->next != phead)
{
if (cur->data == x)
return cur;
else
cur = cur->next;
}
return NULL;
}
- 이 또한 매우 간단하여 연결 리스트를 순회하여 찾고자 하는 값과 노드에 저장된 값을 비교하여 찾으면 현재 노드 위치의 주소를 반환하고 찾지 못하면 NULL을 반환한다.
10. pos의 이전 위치 값을 수정하는 ListInsert 함수
- 여기서 pos 위치의 값을 수정하는 기능에 대해 간단히 말씀드리자면 매우 간단하여 공간을 낭비할 필요가 없습니다 위의 ListFind를 통해 찾아낸 후 바로 이 위치의 데이터를 x 필요합니다.
cur->data=x;
- 제목 속으로
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
ListNode* newnode = BuyListNode(x);//创建结点
ListNode* posPrev = pos->prev;
newnode->next = posPrev->next;
posPrev->next = newnode;
pos->prev = newnode;
newnode->prev = posPrev;
}
- 여기서 pos 위치는 위의 조회 기능으로 찾을 수 있습니다.
- 찾아보면 아주 간단합니다 pos의 위치 이전 노드 posprev의 다음 노드를 newnode로 가리키고, newnode의 prev가 posprev를 가리키게 한 다음 newnode의 다음 노드가 pos를 가리키도록 하고 마지막으로 pos의 prev를 가리키도록 합니다. 삽입된 newnode를 가리킵니다.
11. 위치 pos에서 노드 ListErase를 삭제합니다.
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* posPrev = pos->prev;
ListNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
//pos = NULL;
}
- 이것은 또한 매우 이해하기 쉽습니다. 우리는 pos의 이전 위치에 있는 노드 posPrev가 pos 다음 위치에 있는 노드 posNext를 가리키도록 합니다.
- 마지막 소소한 디테일 최적화, 면접이나 필기시험 등 제한된 시간에 있을 때 앞선 양방향 순환 연결 리스트를 완성해야 합니다. 꼬리 삭제, 위의 내용을 참조하십시오. 이 두 가지 기능은
//头插与头删
ListInsert(phead->next, x);
ListErase(phead->next);
//尾插与尾删
ListInsert(phead, x);
ListErase(phead->prev);
- 왜 그런지 생각해봐
- 사실 우리의 head-plug 삭제와 tail-plug 삭제는 특별한 pos 위치에 대한 연산에 지나지 않으므로 자연스럽게 위의 두 기능을 참조할 수 있습니다.
요약하다
- 오늘의 내용은 여기까지입니다. 양방향 순환 연결 리스트에서 주도권을 잡는 것은 이해하기 쉽지 않을 수 있습니다. 정말 깊이 이해하고 싶다면 논리도를 가지고 두드려 봐야 합니다. 연습하지 않고 배워라 오! !
- 그럼 궁금하신 점은 댓글이나 비밀댓글로 물어봐주세요 다음에 또 만나요!
초보 블로거가 만들기 쉽지 않으니 글의 내용이 도움이 되셨다면 이 초보 블로거를 클릭하셔서 떠나시는 것도 좋을 것 같습니다. 귀하의 지원은 업데이트에 대한 동기 부여입니다! ! !
**(케리 블로거 3연속 응원 부탁드립니다!!! 아래 댓글을 눌러 좋아요를 누르고 케리를 도와주세요)**