一单向链表的插入排序
void insertion_sort_link(link_t* plink)
{ // 如果链表头为空,直接返回 if(NULL == plink->phead) { return; } // 初始化指针,p指向当前已排序部分的最后一个节点 node_t* p = plink->phead; // ptemp指向待插入的下一个节点 node_t* ptemp = plink->phead->pnext; // 将当前已排序部分的最后一个节点的下一个指针设为NULL p->pnext = NULL; node_t* pinsert = NULL; // 用于指向待插入的节点 // 遍历待插入的节点 while(ptemp) { pinsert = ptemp; // 将待插入节点指向ptemp ptemp = ptemp->pnext; // 移动ptemp到下一个节点 // 如果待插入节点的值小于当前已排序部分的头节点的值 if(pinsert->data < p->data) { // 将待插入节点插入到链表头部 pinsert->pnext = plink->phead; plink->phead = pinsert; } else { // 找到待插入节点应该插入的位置 while(p->pnext != NULL && p->pnext->data < pinsert->data) { p = p->pnext; // 移动p到下一个节点 } // 插入待插入节点 pinsert->pnext = p->pnext; // 将待插入节点的下一个指针指向p的下一个节点 p->pnext = pinsert; // 将p的下一个指针指向待插入节点 } } return; // 排序完成,返回
}
快慢指针
快慢指针是一种常用的算法技巧,通常用于链表中,以解决一些特定的问题,比如寻找链表的中间节点、检测链表是否有环等。以下是快慢指针的基本概念和示例代码。
概念
- 慢指针(slow pointer):每次移动一步。
- 快指针(fast pointer):每次移动两步。
通过这种方式,快指针和慢指针的相对速度可以帮助我们在链表中找到特定的节点或判断某些条件。
示例:寻找链表的倒数第K个节点
node_t * countdown_k(link_t* plink, int key)
{ node_t *pfast = plink->phead; // 快指针初始化为链表头 if (NULL == pfast) { return NULL; // 如果链表为空,返回NULL } node_t *pslow = pfast; // 慢指针初始化为链表头 // 将快指针向前移动key个节点 for (int i = 0; i < key; ++i) { if (NULL == pfast) { return NULL; // 如果在移动过程中快指针变为NULL,返回NULL } pfast = pfast->pnext; // 快指针移动到下一个节点 } // 同时移动快指针和慢指针,直到快指针到达链表末尾 while (pfast) { pfast = pfast->pnext; // 快指针移动到下一个节点 pslow = pslow->pnext; // 慢指针也移动到下一个节点 } // 当快指针到达链表末尾时,慢指针指向的就是倒数第key个节点 return pslow; // 返回倒数第key个节点
}
二、双向链表
双向链表是一种链表数据结构,其中每个节点都有两个指针:一个指向下一个节点(next),另一个指向前一个节点(prev)。这种结构使得在链表中进行插入和删除操作更加灵活,因为可以在 O(1) 的时间复杂度内访问前一个节点。
链表结构:
typedef struct node
{ datatype data; // 节点存储的数据 struct node *ppre; // 指向前一个节点的指针 struct node *pnext; // 指向下一个节点的指针
} dnode_t;
双向链表和单向链表的区别
双向链表和单向链表是两种常见的数据结构,它们在结构、操作和使用场景上有显著的区别。以下是它们的主要区别和适用场景:
1. 结构区别
-
单向链表:
- 每个节点包含两个部分:数据部分和指向下一个节点的指针(next)。
- 只能从头到尾遍历链表,无法反向遍历。
-
双向链表:
- 每个节点包含三个部分:数据部分、指向下一个节点的指针(next)和指向前一个节点的指针(prev)。
- 可以从头到尾和从尾到头双向遍历。
2. 操作区别
-
插入和删除:
- 单向链表: 在插入或删除节点时,需要找到前一个节点(需要遍历),操作相对复杂。
- 双向链表: 可以直接访问前一个节点,插入和删除操作更为高效。
-
内存使用:
- 单向链表: 每个节点只需存储一个指针,内存占用较小。
- 双向链表: 每个节点需要存储两个指针,内存占用较大。
3. 使用场景
-
单向链表:
- 适用于只需要单向遍历的场景,如实现简单的队列、栈等。
- 当内存使用是一个重要考虑因素时,单向链表更为合适。
-
双向链表:
- 适用于需要频繁插入和删除操作的场景,如实现双向队列、LRU缓存等。
- 当需要频繁反向遍历链表时,双向链表更为高效。
4. 性能比较
- 时间复杂度:
- 插入和删除操作在单向链表中通常需要O(n)的时间复杂度(需要遍历找到前一个节点),而在双向链表中可以在O(1)的时间内完成(直接访问前后节点)。
- 空间复杂度:
- 单向链表的空间复杂度为O(n),双向链表的空间复杂度也是O(n),但由于双向链表每个节点需要额外的指针,实际内存占用更高。
双向链表的基本操作:
// 创建双向链表
dlink_t* create_dlink() { dlink_t * dlink = malloc(sizeof(dlink_t)); // 分配内存 if (NULL == dlink) { return NULL; // 内存分配失败 } dlink->phead = NULL; // 初始化头指针为空 dlink->clen = 0; // 初始化节点计数为0 pthread_mutex_init(&(dlink->mutex), NULL); // 初始化互斥锁 return dlink; // 返回新创建的链表
} // 检查链表是否为空
int is_empty_dlink(dlink_t * pdlink) { return NULL == pdlink->phead; // 如果头指针为空,则链表为空
} // 在链表头部插入节点
int push_into_dlink_head(dlink_t* pdlink, datatype data) { dnode_t * pnode = malloc(sizeof(dnode_t)); // 分配新节点内存 if (NULL == pnode) { return -1; // 内存分配失败 } pnode->data = data; // 设置节点数据 pnode->ppre = NULL; // 新节点的前指针为空 pnode->pnext = NULL; // 新节点的后指针为空 if (is_empty_dlink(pdlink)) { // 如果链表为空 pdlink->phead = pnode; // 将新节点设置为头节点 } else { // 如果链表不为空 pnode->pnext = pdlink->phead; // 新节点的后指针指向当前头节点 pdlink->phead->ppre = pnode; // 当前头节点的前指针指向新节点 pdlink->phead = pnode; // 更新头指针为新节点 } pdlink->clen++; // 增加节点计数 return 0; // 插入成功
} // 打印链表
int print_link(dlink_t * pdlink, int dir) { dnode_t* p = pdlink->phead; // 从头节点开始 if (NULL == p) { return -1; // 如果链表为空,返回错误 } if (dir) { // 正向打印 while (p) { printf("id:%d,name:%s,score:%d\n", p->data.id, p->data.name, p->data.score); // 打印节点数据 p = p->pnext; // 移动到下一个节点 } printf("\n"); } else { // 反向打印 while (p->pnext) { p = p->pnext; // 移动到最后一个节点 } while (p) { printf("id: %d,name: %s,score: %d\n", p->data.id, p->data.name, p->data.score); // 打印节点数据 p = p->ppre; // 移动到前一个节点 } printf("\n"); } return 0; // 打印成功
} // 在链表尾部插入节点
int push_into_dlink_tail(dlink_t* pdlink, datatype data) { if (is_empty_dlink(pdlink)) { // 如果链表为空 push_into_dlink_head(pdlink, data); // 直接在头部插入 return 0; // 插入成功 } dnode_t* p = pdlink->phead; // 从头节点开始 dnode_t* pnode = malloc(sizeof(dnode_t)); // 分配新节点内存 pnode->data = data; // 设置节点数据 pnode->ppre = NULL; // 新节点的前指针为空 pnode->pnext = NULL; // 新节点的后指针为空 if (NULL == pnode) { return -1; // 内存分配失败 } while (p->pnext) { // 找到最后一个节点 p = p->pnext; } pnode->ppre = p; // 新节点的前指针指向最后一个节点 p->pnext = pnode; // 最后一个节点的后指针指向新节点 pdlink->clen++; // 增加节点计数 return 0; // 插入成功
} // 从链表头部删除节点
int pop_into_dlink_head(dlink_t* pdlink) { if (is_empty_dlink(pdlink)) { return -1; // 如果链表为空,返回错误 } dnode_t* p = pdlink->phead; // 获取头节点 pdlink->phead = pdlink->phead->pnext; // 更新头指针为下一个节点 free(p); // 释放头节点内存 pdlink->clen--; // 减少节点计数 if (pdlink->phead == NULL) { return 0; // 如果链表变为空,返回成功 } if (pdlink->phead->pnext) { pdlink->phead->ppre = NULL; // 更新新头节点的前指针为空 } return 0; // 删除成功
} // 从链表尾部删除节点
int pop_into_dlink_tail(dlink_t* pdlink) { if (is_empty_dlink(pdlink)) { return -1; // 如果链表为空,返回错误 } dnode_t* p = pdlink->phead; // 从头节点开始 while (p->pnext) { // 找到最后一个节点 p = p->pnext; } if (p->ppre == NULL) { // 如果只有一个节点 free(p); // 释放节点内存 pdlink->clen--; // 减少节点计数 pdlink->phead = NULL; // 更新头指针为空 return 0; // 删除成功 } p->ppre->pnext = NULL; // 更新倒数第二个节点的后指针为空 free(p); // 释放最后一个节点内存 pdlink->clen--; // 减少节点计数 return 0; // 删除成功
} // 根据名称搜索节点
dnode_t* search_into_dlink(dlink_t* pdlink, char * name) { if (is_empty_dlink(pdlink)) { return NULL; // 如果链表为空,返回NULL } dnode_t* p = pdlink->phead; // 从头节点开始 while (p) { if (strcmp(p->data.name, name) == 0) { // 如果找到匹配的名称 return p; // 返回找到的节点 } p = p->pnext; // 移动到下一个节点 } return NULL; // 未找到匹配的节点
} // 修改节点数据
int change_dlink_node(dlink_t* pdlink, char *name, int dstscore) { if (is_empty_dlink(pdlink)) { return -1; // 如果链表为空,返回错误 } dnode_t* p = pdlink->phead; // 从头节点开始 while (p) { if (strcmp(p->data.name, name) == 0) { // 如果找到匹配的名称 p->data.score = dstscore; // 更新节点的分数 } p = p->pnext; // 移动到下一个节点 } return 0; // 修改成功
} // 销毁链表
int destory_dlink(dlink_t* pdlink) { while (pdlink->phead) { pop_into_dlink_head(pdlink); // 逐个删除头节点 } print_link(pdlink, 1); // 打印链表(此时应为空) free(pdlink); // 释放链表结构体内存 return 0; // 销毁成功
}