目录
前言
排序可以说是每一个计算机爱好者最先接触的算法之一,在C语言中,我们最先是通过数组来学习排序的,由于数组可以直接通过下标对数据进行操作,为我们理解排序算法和实现排序操作提供了许多便利。那么当我们需要对链表进行排序时又该怎么操作呢?下面整理了四种链表排序的思路和具体的代码,在每种链表排序之前,会先复习一下数组中这种排序是如何实现的哦!
插入排序
什么是插入排序?简单来说,就是将每一个元素依次找到它应该插入的位置,当每一个元素都被插入到了它应该被插入的位置时,整个表就完成了排序。
数组插入排序
假设第一个元素为有序表,从第二个元素开始依次查找每个元素应该插入的位置,但是如果该元素本身就符合顺序,则不用调整该元素的位置。这里演示一下从小到大做插排。
第二个元素比第一个元素小,将-3提到4之前
换位完成后,调整第三个元素。第三个元素为7,符合从小到大的顺序,不用调整。
然后是最后一个元素5,比4大且比7小,所以要插入到7之前。
链表插入排序
链表的插入排序与数组的思想类似,找到需要换位的结点,再寻找该结点要插入的位置。在整个排序过程中,我们一共需要三个指针tail、cur、pre,cur用来遍历链表,tail用来确定已排好序的部分,pre用来找到cur要插入的位置。首先来看怎样寻找需要改变位置的结点。
找结点
struct ListNode *tail = head;
struct ListNode *cur = head->next;
while (cur != NULL) {
if (tail->val <= cur->val) {
tail = tail->next;
cur = tail->next;
}
这样,cur就指向了要改变位置的那个结点,而tail指向排好位置的最后一个结点。接下来来查找cur要插入的位置并改链。
找位置
struct ListNode *pre = shead;
while (pre->next->val <= cur->val) {
pre = pre->next;
} //查找位置
tail->next = cur->next;
cur->next = pre->next;
pre->next = cur; //改链
cur = tail->next; //从下一个结点开始遍历
这里改链的操作如图:
完整的代码如下:
struct ListNode *insertionSortList(struct ListNode *head) {
if (head == NULL) {
return head;
}
struct ListNode *shead = malloc(sizeof(struct ListNode));
shead->val = 0;
shead->next = head; //设置虚拟头结点
struct ListNode *tail = head;
struct ListNode *cur = head->next;
while (cur != NULL) {
if (tail->val <= cur->val) {
tail = tail->next;
} else {
struct ListNode *pre = shead;
while (pre->next->val <= cur->val) {
pre = pre->next;
}
tail->next = cur->next;
cur->next = pre->next;
pre->next = cur;
}
cur = tail->next;
}
return shead->next;
}
选择排序
选择排序是最符合人的直觉的一种排序方式,以从小到大排序为例,选择排序就是在所有数中选择最小的那一个放入已排序的表中,再在剩下的元素中选择最小的一个元素放入排好序的尾部,这样选择完最后一个元素时,就会得到一个从小到大的数组。
数组选择排序
首先选择排在第一个位置的元素,在整个数组中找到最小的那个元素,也就是-4,将-4与第一个元素7交换位置。
然后寻找第二个位置的元素,在除掉第一个元素的剩下的数组中找到最小的元素,也就是3,将3与5互换位置。
再找第三个位置的元素,剩下的元素为5、7,5为最小值,则不用交换。
最后得到一个从小到大排列的数组
链表选择排序
链表的选择排序思路与数组类似,以从小到大排序为例,要先确定选择元素插入的位置,再在剩下的表中找到最小值的结点,将该节点插入之前确定的位置中。因此我们就需要五个指针tail,cur,cur_pre,min,min_pre。tail同样用来确定已经排好序的部分,cur用来遍历,而min用来记录最小值,cur_pre和min_pre分别是cur和pre之前的那个结点,用来改链。
首先来看如何确定要选择元素插入的位置。
定位置
for (tail = shead; tail != NULL && tail->next != NULL; tail = tail->next) {
min = tail->next;
}
由于要选择元素插入的位置就是从前到后的每一个位置,所以直接遍历即可,确定位置后,假设该位置元素为最小值,再与之后的元素比较,如果后续元素中的最小值大于该元素,则该元素就是真正的最小值,则不用交换位置,直接开始处理下一个位置;如果后续元素中最小值小于该元素,则将该元素与最小值交换。找元素的过程我们用cur来遍历tail之后的链表即可实现。具体的代码实现如下:
找结点
for (cur = tail->next, cur_pre = tail; cur != NULL; cur_pre = cur, cur = cur->next) {
if (cur->val < min->val) {
min = cur;
min_pre = cur_pre; //找最小结点
}
}
if (tail->next != min) {
min_pre->next = min->next;
min->next = tail->next;
tail->next = min; //改链
}
ps:这里的if语句是因为前文假设tail->next = min,如果最后min == tail->next,说明没有找到比tail->next更小的值,则该位置不用交换元素。
这里改链的操作如图:
完整的代码如下:
struct ListNode* insertionSortList(struct ListNode* head) {
struct ListNode* shead = (struct ListNode*)malloc(sizeof(struct ListNode));
shead->next = head; //设置虚拟头结点
struct ListNode* tail, *cur_pre, *cur, *min, *min_pre;
for (tail = shead; tail != NULL && tail->next != NULL; tail = tail->next) {
min = tail->next;
for (cur = tail->next, cur_pre = tail; cur != NULL; cur_pre = cur, cur = cur->next){
if (cur->val < min->val) {
min = cur;
min_pre = cur_pre;
}
}
if (tail->next != min) {
min_pre->next = min->next;
min->next = tail->next;
tail->next = min;
}
}
head = shead->next;
free(shead);
return head;
}
冒泡排序
冒泡排序是一种符合计算机逻辑的排序,以从小到大排序为例,每两个元素比较一次,并让这两个元素按照要求的顺序排列,这样一轮可以确定一个最大值,在n-1轮后,就可以得到一个有序的表。这样像泡泡一样从前往后走就叫冒泡排序
数组冒泡排序
第一轮冒泡,前两个元素比较,7>5,互换位置
然后7与3比较,互换位置
然后7与-4比较,互换位置
一轮冒泡完成后,确定了7在末尾,接下来开始第二轮冒泡,5大于3,互换位置
5大于-4,互换位置
第三轮冒泡,3大于-4,互换位置
这样就得到了一个从小到大排列的数组
链表冒泡排序
链表的冒泡排序同样也是两个结点两个结点比较,经过n-1轮冒泡后,就可以得到一个有序的表,而这里要实现计数n-1轮,就需要一个tail指针来记录已经排好序的部分,每次让指针遍历到tail就停止就好了,除此之外,我们还需要两个指针,cur用来遍历,pre是cur前一个结点,用来对cur改链。首先来看怎样实现统计轮数
记轮数
while (shead->next != NULL && shead->next->next != tail) {
pre = shead;
cur = shead->next;
while (cur->next != tail) {
pre = pre->next;
cur = pre->next;
} //cur移动到tail之前的那一个元素
tail = cur; //tail向前进一位
}
这样tail每次都向前进一位,也就实现了统计已排好序的结点和统计轮数的功能。
两两比较
while (cur->next != tail) {
if (cur->val > cur->next->val) {
pre->next = cur ->next;
cur->next = pre->next->next;
pre->next->next = cur;
} //改链
pre = pre->next;
cur = pre->next;
}
两两比较,如顺序不对则换位,这里的改链操作如下
完整的代码如下:
struct ListNode *insertionSortList(struct ListNode *head) {
struct ListNode* shead = (struct ListNode*)malloc(sizeof(struct ListNode));
shead->next = head; //设置虚拟头结点
struct ListNode* tail = NULL, *cur, *pre;
while (shead->next != NULL && shead->next->next != tail) {
pre = shead;
cur = shead->next;
while (cur->next != tail) {
if (cur->val > cur->next->val) {
pre->next = cur ->next;
cur->next = pre->next->next;
pre->next->next = cur;
}
pre = pre->next;
cur = pre->next;
}
tail = cur;
}
head = shead->next;
free(shead);
return head;
return shead->next;
}
快速排序
快速排序是一种利用递归完成的排序,以升序为例,设定一个基准值,比基准值小的放在左边,比基准值大的放在右边。这样表就被分成了两个新的空间,在新的空间里又设定新的基准值重复上述操作,这样通过递归就可以得到一个标准的升序排列的表。
数组快速排序
先设定基准值为第二个元素3,将比3小的元素放在左边,大的元素放在右边
得到两个新的区间,设新区间的基准值分别是-4和7,-4小于0,位置不变。7大于5,5放到7的左边
这样就得到了两个有序区间,这两个有序区间合并就是一个有序的数组。
链表快速排序
链表快速排序的思路与数组一样,也是通过基准值不断分区间,直到有序。这里我们需要基准值privot,还需要两个指针,cur用来遍历区间内结点,pre是cur前一个结点,用来对cur改链。我们先来看一看如何分区间
分区间
void quick_sort(struct ListNode* head, struct ListNode* tail)
{
if(head == NULL || head->next == tail || head->next->next == tail)
{
return ;
}
struct ListNode *privot = partition(head, tail);
quick_sort(head, privot);
quick_sort(privot, tail);
}
我们用一个函数patition返回区间内的基准值来划分出更小的区间,并对该区间递归调用,区间不断缩小,直到区间内只剩一个元素。
结点分类
struct ListNode* partition(struct ListNode* head, struct ListNode* tail) {
struct ListNode *cur, *pre, *privot;
privot = head->next;
for(pre = privot, cur = privot->next; cur && cur != tail; cur = pre->next)
{
if(privot->val > cur->val)
{
pre->next = cur->next;
cur->next = head->next;
head->next = cur; //改链
}
else
{
pre = pre->next;
}
}
return privot;
}
对区间内结点分类,比基准值小的在一边,比基准值大的在另一边。设基准值为区间内第一个元素,那么只需比较之后元素中比基准值大的,则直接跳过,比基准值小的,就放到基准值前,也就是表头就好。这里的改链操作如下:
完整的代码如下:
struct ListNode* partition(struct ListNode* head, struct ListNode* tail) {
struct ListNode *cur, *pre, *privot;
privot = head->next;
for(pre = privot, cur = privot->next; cur && cur != tail; cur = pre->next)
{
if(privot->val > cur->val)
{
pre->next = cur->next;
cur->next = head->next;
head->next = cur;
}
else
{
pre = pre->next;
}
}
return privot;
}
void quick_sort(struct ListNode* head, struct ListNode* tail)
{
if(head == NULL || head->next == tail || head->next->next == tail)
{
return ;
}
struct ListNode *privot = partition(head, tail);
quick_sort(head, privot);
quick_sort(privot, tail);
}
学习完了四种链表的排序方法,去做道题练习一下吧,下面给出了一道力扣上的链表排序,这道题没有时间复杂度的要求,可以将上述四种排序都练习一遍噢!