1.书接上文
如何找到环的入口?
- 先确定环有几个节点,fast和slow相遇的那个节点,设置一个p指向此节点,count来计数
- 让p走,直到p再次遇到fast或者让fast走直到fast再次等于p
此时count=6,意味着环有六个节点
说明第一次循环fast指针比slow指针多走六步
- 重新让fast和slow指针指向头节点,fast比slow先走6步,然后同步走.下次相遇的时候指向的就是环的入口
// 找到链表环的入口节点
Node* findBegin(Node *head)
{
Node *fast = head;
Node *slow = head;
// 第一步:判断是否有环
while (fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
// 如果快慢指针相遇,说明有环
if (fast == slow)
{
// 第二步:计算环的长度
Node *p = fast;
int count = 1;
while (p->next != slow)
{
count++;
p = p->next;
}
// 第三步:找环的入口
fast = head;
slow = head;
// 让 fast 先走 count 步
for (int i = 0; i < count; i++)
{
fast = fast->next;
}
// fast 和 slow 同时前进,相遇处就是环的入口
while (fast != slow)
{
fast = fast->next;
slow = slow->next;
}
return slow; // 返回环入口节点
}
}
return NULL; // 无环返回空
}
完整例子:
#include <stdio.h>
#include <stdlib.h>
typedef int ElemType;
// 定义链表节点结构
typedef struct node {
ElemType data;
struct node *next;
} Node;
// 创建一个空链表(带头结点)
Node* initList() {
Node *head = (Node*)malloc(sizeof(Node));
head->next = NULL;
return head;
}
// 尾插法建立链表
Node* insertTail(Node *head, ElemType e) {
Node *p = (Node*)malloc(sizeof(Node));
p->data = e;
p->next = NULL;
Node *tail = head;
while (tail->next != NULL)
tail = tail->next;
tail->next = p;
return head;
}
// 打印链表(最多打印20个,防止死循环)
void printList(Node *head) {
Node *p = head->next;
int count = 0;
while (p != NULL && count < 20) {
printf("%d -> ", p->data);
p = p->next;
count++;
}
printf("NULL\n");
}
// 查找环的入口
Node* findBegin(Node *head) {
Node *fast = head;
Node *slow = head;
// 第一步:判断是否有环
while (fast != NULL && fast->next != NULL) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) {
// 第二步:计算环的长度
Node *p = fast;
int count = 1;
while (p->next != slow) {
count++;
p = p->next;
}
// 第三步:寻找环的入口
fast = head;
slow = head;
for (int i = 0; i < count; i++) {
fast = fast->next;
}
while (fast != slow) {
fast = fast->next;
slow = slow->next;
}
return slow; // 环入口
}
}
return NULL; // 无环
}
int main() {
// 创建链表
Node *head = initList();
for (int i = 1; i <= 6; i++) {
insertTail(head, i);
}
// 手动制造一个环:让最后一个节点指向第3个节点
Node *p = head->next;
Node *third = NULL;
Node *tail = NULL;
int count = 1;
while (p != NULL) {
if (count == 3) third = p; // 记录第3个节点
if (p->next == NULL) tail = p; // 记录最后一个节点
p = p->next;
count++;
}
if (tail && third)
tail->next = third; // 形成环
// 检测并找到环的入口
Node *entry = findBegin(head);
if (entry != NULL)
printf("链表存在环,入口节点的值为:%d\n", entry->data);
else
printf("链表无环。\n");
return 0;
}
2.双向链表
链式存储结构的节点中只有一个指示直接后继的指针域,由此,从某个结点出发只能顺指针向后寻查其他节点。若要寻查结点的直接前驱,则必须从表头指针出发。换句话说,在单链表中,查找直接后继的执行时间为 O(1),而查找直接前驱的执行时间为 O(n)。
为克服单链表这种单向性的缺点,可利用双向链表(Double Linked List)。在双向链表的节点中有两个指针域,一个指向直接后继,另一个指向直接前驱
typedef int ElemType;
typedef struct node
{
ElemType data;
struct node *prev,*next;
}Node;
- 头插法
// 在 双向链表 的 头结点 L 之后,插入一个新节点 p
int insertHead(Node* L,ElemType e){
Node *p=(Node*)malloc(sizeof(Node));
p->data=e;
p->prev=L;
p->next=L->next;
// 如果原本链表不为空(也就是 头节点L之后确实还有节点),
// 那么原来第一个节点(即 L->next)的 prev 指针,
// 要从原来的 L 改成现在的 p
if (L->next!=NULL)
{
L->next->prev=p;
}
L->next=p;
return 1;
}
- 尾插法
// 获取尾部节点
Node* get_tail(Node *L){
Node *p=L;
while (p->next!=NULL)
{
p=p->next;
}
return p;
}
// 尾插法
Node* insertTail(Node *tail,ElemType e){
Node *p=(Node*)malloc(sizeof(Node));
p->data=e;
p->prev=tail;
tail->next=p;
p->next=NULL;
return p;
}
- 在指定位置插入数据
// 指定位置插入
int insertNode(Node *L,int pos,ElemType e){
Node *p=L;
int i=0;
while (i<pos-1)
{
p=p->next;
i++;
if (p==NULL)
{
return 0;
}
}
Node *q= (Node*)malloc(sizeof(Node));
q->data=e;
q->prev=p;
q->next=p->next;
p->next->prev=q;
p->next=q;
return 1;
}
- 删除节点
- 找到要删除节点的前置节点p
- 用指针q记录要删除的节点
- 通过改变p的后置节点及要删除节点的下一个节点的前驱实现删除
- 释放删除节点的空间
int deleteNode(Node *L,int pos){
Node *p=L;
int i=0;
while (i<pos-1)
{
p=p->next;
i++;
if (p==NULL)
{
return 0;
}
}
if (p->next==NULL)
{
printf("要删除的位置错误\n");
return 0;
}
Node *q=p->next;
p->next=q->next;
q->next->prev=p;
free(q);
return 1;
}
汇总:
// 双向链表
#include <stdio.h>
#include <stdlib.h>
typedef int ElemType;
typedef struct node
{
ElemType data;
struct node *prev,*next;
}Node;
// 初始化链表
Node* initList(){
Node *head=(Node*)malloc(sizeof(Node));
head->data=0;
head->next=NULL;
head->prev=NULL;
return head;
}
// 头插法
// 在 双向链表 的 头结点 L 之后,插入一个新节点 p
int insertHead(Node* L,ElemType e){
Node *p=(Node*)malloc(sizeof(Node));
p->data=e;
p->prev=L;
p->next=L->next;
// 如果原本链表不为空(也就是 头节点L之后确实还有节点),
// 那么原来第一个节点(即 L->next)的 prev 指针,
// 要从原来的 L 改成现在的 p
if (L->next!=NULL)
{
L->next->prev=p;
}
L->next=p;
return 1;
}
// 遍历
void listNode(Node* L){
Node *p=L->next;
while (p!=NULL)
{
printf("%d ",p->data);
p=p->next;
}
printf("\n");
}
// 获取尾部节点
Node* get_tail(Node *L){
Node *p=L;
while (p->next!=NULL)
{
p=p->next;
}
return p;
}
// 尾插法
Node* insertTail(Node *tail,ElemType e){
Node *p=(Node*)malloc(sizeof(Node));
p->data=e;
p->prev=tail;
tail->next=p;
p->next=NULL;
return p;
}
// 指定位置插入
int insertNode(Node *L,int pos,ElemType e){
Node *p=L;
int i=0;
while (i<pos-1)
{
p=p->next;
i++;
if (p==NULL)
{
return 0;
}
}
Node *q= (Node*)malloc(sizeof(Node));
q->data=e;
q->prev=p;
q->next=p->next;
p->next->prev=q;
p->next=q;
return 1;
}
// 删除节点
int deleteNode(Node *L,int pos){
Node *p=L;
int i=0;
while (i<pos-1)
{
p=p->next;
i++;
if (p==NULL)
{
return 0;
}
}
if (p->next==NULL)
{
printf("要删除的位置错误\n");
return 0;
}
Node *q=p->next;
p->next=q->next;
q->next->prev=p;
free(q);
return 1;
}
// 释放链表
void freeList(Node *L){
Node *p=L->next;
Node *q;
while (p!=NULL)
{
q=p->next;
free(p);
p=q;
}
L->next=NULL;
}
int main(){
Node *list=initList();
insertHead(list,10);
insertHead(list,20);
insertHead(list,30);
listNode(list);
Node *tail=get_tail(list);
tail=insertTail(tail,10);
tail=insertTail(tail,20);
tail=insertTail(tail,30);
listNode(list);
insertNode(list,2,15);
listNode(list);
deleteNode(list,2);
listNode(list);
}
至此,线性表已全部讲完
顺序表VS链表
比较项目 | 存储结构 | 顺序表 | 链表 |
---|---|---|---|
空间 | 存储空间 | 预先分配,会出现闲置或溢出现象 | 动态分配,不会出现存储空间闲置或溢出现象 |
存储密度 | 不用为表示节点间的逻辑关系而增加额外的存储,存储密度等于1 | 需要借助指针来体现元素间的逻辑关系,存储密度小于1 | |
时间 | 存取元素 | 随机存取,按位置访问元素的时间复杂度为 O(1) | 顺序存取,按位置访问元素时间复杂度为 O(n) |
插入、删除 | 平均移动约表中一半元素,时间复杂度为 O(n) | 不需要移动元素,确定插入、删除位置后,时间复杂度为 O(1) | |
适用情况 | 1)表长变化不大,且能事先确定变化的范围; 2)很少进行插入或删除操作,经常按位置查找元素序号访问数据元素。 | 1)长度变化较大; 2)频繁进行插入或删除操作。 |
做个题
- 第一题
链接地址就是next
答案是D
- 第二题
答案:D
- 第三题
注意!链表可能就一个头节点一个尾节点
答案:D
!!!也可能p和q是同一个节点
3.栈
栈(stack)是限定仅在表尾进行插入或删除操作的线性表。因此对栈来说,表尾端有其特殊含义,称为栈顶(top),相应地,表头端称为栈底(bottom)。不含元素的空表称为空栈
假设 S = (a₁, a₂, ⋯, aₙ),则称 a₁ 为栈底元素,aₙ 为栈顶元素。栈中元素按 a₁, a₂, ⋯, aₙ 的次序进栈,退栈的第一个元素应为栈顶元素。换句话说,栈的修改是按照后进先出的原则进行的。因此,栈又称为后进先出(Last In First Out,LIFO)的线性表
先进后出,后进先出
栈是限制插入和删除操作只能在一个位置进行的表,该位置是表的末端,叫做栈顶(top).对栈的基本操作有进栈(push)和出栈(Pop),前者表示插入,后者则是删除最后插入的元素
栈的顺序结构实现
- 初始化
#define MAXSIZE 100
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];
int top;
}Stack;
void initStack(Stack *s){
s->top=-1;
}
- 判断栈是否为空
int isEmpty(Stack *s){
if (s->top==-1)
{
printf("空的\n");
return 1;
}
else{
return 0;
}
}
- 进栈/压栈
int push(Stack *s,ElemType e){
if (s->top>=MAXSIZE-1)
{
printf("满了\n");
return 0;
}
s->top++;
s->data[s->top]=e;
return 1;
}
- 出栈
ElemType pop(Stack *s,ElemType *e){
if (s->top==-1)
{
printf("空的\n");
return 0;
}
*e=s->data[s->top];
s->top--;
return 1;
}
- 获取栈顶元素
int getTop(Stack *s,ElemType *e){
if (s->top==-1)
{
printf("空的\n");
return 0;
}
*e=s->data[s->top];
return 1;
}
汇总
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];
int top;
}Stack;
void initStack(Stack *s){
s->top=-1;
}
// 判断栈是否为空
int isEmpty(Stack *s){
if (s->top==-1)
{
printf("空的\n");
return 1;
}
else{
return 0;
}
}
// 压栈
int push(Stack *s,ElemType e){
if (s->top>=MAXSIZE-1)
{
printf("满了\n");
return 0;
}
s->top++;
s->data[s->top]=e;
return 1;
}
// 出栈
ElemType pop(Stack *s,ElemType *e){
if (s->top==-1)
{
printf("空的\n");
return 0;
}
*e=s->data[s->top];
s->top--;
return 1;
}
// 获取栈顶元素
int getTop(Stack *s,ElemType *e){
if (s->top==-1)
{
printf("空的\n");
return 0;
}
*e=s->data[s->top];
return 1;
}
int main(){
Stack s;
initStack(&s);
push(&s,10);
push(&s,20);
push(&s,30);
ElemType e;
pop(&s,&e);
printf("%d\n",e);
getTop(&s,&e);
printf("%d\n",e);
}
- 栈的顺序结构的初始化
#define MAXSIZE 100
typedef int ElemType;
typedef struct
{
ElemType *data; //指针指向堆内存一片连续的空间
int top;
}Stack;
Stack* initStack(){
Stack *s=(Stack*)malloc(sizeof(Stack));
s->data=(ElemType*)malloc(sizeof(ElemType)*MAXSIZE);
s->top=-1;
return s;
}
栈的链式实现
typedef int ElemType;
typedef struct stack
{
ElemType data;
struct stack *next;
}Stack;
尾节点是栈底
头节点的下一个节点就是栈顶
压栈其实就是头插法
栈空就是只有一个头节点,也就是头节点的next是否为NULL
获取栈顶数据—找头节点的下一个节点的data
- 初始化
Stack* initStack(){
Stack* s=(Stack*)malloc(sizeof(Stack));
s->data=0;
s->next=NULL;
return s;
}
- 判断栈是否为空
int isEmpty(Stack *s){
if (s->next==NULL)
{
printf("空的\n");
return 0;
}
else{
return 0;
}
}
- 进栈/压栈
int push(Stack *s,ElemType e){
Stack *p=(Stack*)malloc(sizeof(Stack));
p->data=e;
p->next=s->next;
s->next=p;
return 1;
}
- 出栈
删除头节点后的那一个节点的数据:
- 找到头节点p
- 用指针q记录要出栈的节点
- 通过改变q的后继节点实现出栈
int pop(Stack *s,ElemType *e){
if (s->next==NULL)
{
printf("空的\n");
return 0;
}
*e=s->next->data;
Stack *q=s->next;
s->next=q->next;
free(q);
return 1;
}
- 获取栈顶元素
int getTop(Stack *s,ElemType *e){
if (s->next==NULL)
{
printf("空的\n");
return 0;
}
*e=s->next->data;
return 1;
}
汇总:
// 栈的链式结构实现
#include <stdio.h>
#include <stdlib.h>
typedef int ElemType;
typedef struct stack
{
ElemType data;
struct stack *next;
}Stack;
Stack* initStack(){
Stack* s=(Stack*)malloc(sizeof(Stack));
s->data=0;
s->next=NULL;
return s;
}
// 判断栈是否为空
int isEmpty(Stack *s){
if (s->next==NULL)
{
printf("空的\n");
return 0;
}
else{
return 0;
}
}
// 进栈/压栈
int push(Stack *s,ElemType e){
Stack *p=(Stack*)malloc(sizeof(Stack));
p->data=e;
p->next=s->next;
s->next=p;
return 1;
}
// 出栈
int pop(Stack *s,ElemType *e){
if (s->next==NULL)
{
printf("空的\n");
return 0;
}
*e=s->next->data;
Stack *q=s->next;
s->next=q->next;
free(q);
return 1;
}
// 获取栈顶元素
int getTop(Stack *s,ElemType *e){
if (s->next==NULL)
{
printf("空的\n");
return 0;
}
*e=s->next->data;
return 1;
}
int main(){
Stack *s=initStack();
push(s,10);
push(s,20);
push(s,30);
ElemType e;
pop(s,&e);
printf("%d\n",e);
getTop(s,&e);
printf("%d\n",e);
}
4.队列
队列(queue)是一种先进先出(First In First Out,FIFO)的线性表。它只允许在表的一端进行插入,而在另一端删除元素。在队列中,允许插入的一端称为队尾(rear),允许删除的一端则称为队头(front)。假设队列为 q = (a₁, a₂, ⋯, aₙ),那么,a₁ 就是队头元素,aₙ 就是队尾元素。队列中的元素是按照 a₁, a₂, ⋯, aₙ 的顺序进入的,退出队列也只能按照这个次序依次退出,也就是说,只有在 a₁, a₂, ⋯, aₙ₋₁ 都离开队列之后,aₙ 才能退出队列。
- 初始化
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];
int front;
int rear;
}Queue;
void initQueue(Queue *Q){
Q->front=0;
Q->rear=0;
}
- 判断队列是否为空
int isEmpty(Queue *Q){
if (Q->front==Q->rear)
{
printf("空的\n");
return 1;
}
else{
return 0;
}
}
- 入队
int equeue(Queue *Q,ElemType e){
if (Q->rear>=MAXSIZE)
{
// 发现对尾到头了
if (!queueFull(Q))
{
return 0;
}
}
Q->data[Q->rear]=e;
Q->rear++;
return 1;
}
- 出队
ElemType dequeue(Queue *Q){
if (Q->front==Q->rear)
{
printf("空的\n");
return 0;
}
ElemType e=Q->data[Q->front];
Q->front++;
return e;
}
- 对尾满了,调整队列
int queueFull(Queue *Q){
if (Q->front>0)
{
int step=Q->front;
for (int i = Q->front; i <= Q->rear; ++i)
{
Q->data[i-step]=Q->data[i];
}
Q->front=0;
Q->rear=Q->rear-step;
return 1;
}
else{
printf("真的满了\n");
return 0;
}
}
- 获取对头元素
int getHead(Queue *Q,ElemType *e){
if (Q->front==Q->rear)
{
printf("空的\n");
return 0;
}
*e=Q->data[Q->front];
return 1;
}
汇总:
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];
int front;
int rear;
}Queue;
void initQueue(Queue *Q){
Q->front=0;
Q->rear=0;
}
// 预声明,避免隐式声明告警
int queueFull(Queue *Q);
// 判断队列是否为空
int isEmpty(Queue *Q){
if (Q->front==Q->rear)
{
printf("空的\n");
return 1;
}
else{
return 0;
}
}
// 出队
ElemType dequeue(Queue *Q){
if (Q->front==Q->rear)
{
printf("空的\n");
return 0;
}
ElemType e=Q->data[Q->front];
Q->front++;
return e;
}
// 入队
int equeue(Queue *Q,ElemType e){
if (Q->rear>=MAXSIZE)
{
if (!queueFull(Q))
{
return 0;
}
}
Q->data[Q->rear]=e;
Q->rear++;
return 1;
}
// 队尾满了,调整队列
int queueFull(Queue *Q){
if (Q->front>0)
{
int step=Q->front;
for (int i = Q->front; i <= Q->rear; ++i)
{
Q->data[i-step]=Q->data[i];
}
Q->front=0;
Q->rear=Q->rear-step;
return 1;
}
else{
printf("真的满了\n");
return 0;
}
}
// 获取队头元素
int getHead(Queue *Q,ElemType *e){
if (Q->front==Q->rear)
{
printf("空的\n");
return 0;
}
*e=Q->data[Q->front];
return 1;
}
int main(){
Queue q;
initQueue(&q);
equeue(&q,10);
equeue(&q,20);
equeue(&q,30);
equeue(&q,40);
equeue(&q,50);
printf("%d\n",dequeue(&q));
printf("%d\n",dequeue(&q));
ElemType e;
getHead(&q,&e);
printf("%d\n",e);
}