雙向迴圈連結串列的插入排序
前兩篇博文,我討論了連結串列的氣泡排序和選擇排序(以Linux核心連結串列為例),這篇文章,我想說說插入排序。
一、複習陣列的插入排序
插入排序在演算法思想中屬於“減治法”。
減治法的基本思想是:規模為n
的原問題的解與較小規模的子問題的解之間具有某種關係。由於存在這種關係,所以只需求解其中一個較小規模的子問題就可以得到原問題的解。
插入排序就是基於“減治法”中的“減一技術”實現的。如果題目要求對a[0]
到a[n-1]
進行排序,我幻想從a[0]
到a[n-2]
已經是有序的了,那麼我需要做的就是在這些有序的元素中為a[n-1]
找到一個合適的位置,然後把它插到那裡就行。
雖然插入排序基於遞迴思想,可從頂至下實現;但是,從底至上地實現這個演算法——使用迭代會效率更高。從a[1]
a[n-1]
為止,a[i]
被插入到陣列的前i
個有序元素中的適當位置上(但是,和選擇排序不同,這個位置一般來說並不是它的最終位置)。
示意圖如下:
二、核心連結串列的插入排序
#include <stdio.h>
#include "list.h"
struct data_info {
int data;
struct list_head list;
};
int cmp_data(struct list_head *a, struct list_head *b)
{
struct data_info *pa = list_entry(a, struct data_info, list);
struct data_info *pb = list_entry(b, struct data_info, list);
return pa->data - pb->data;
}
void swap(struct list_head *a, struct list_head *b)
{
struct list_head flag = {NULL, NULL};
__list_add(&flag, b->prev, b);
list_del(b);
__list_add(b, a->prev, a);
list_del(a);
__list_add(a, flag.prev, &flag);
list_del(&flag);
}
void insert_sort(struct list_head *head,
int(*cmp)(struct list_head *a,
struct list_head *b))
{
struct list_head *i, *j,*temp;
i = head->next->next; //i指向第2個結點
list_for_each_from(i,head){ //i從第2個結點開始遍歷,因為第1個已經有序
j = i->prev; //j指向i的前一個結點
if (cmp(j, i) <= 0) //從表頭開始,按照升序排列
continue;
list_for_each_reverse_continue(j,head){
if(cmp(j,i) <= 0)
break;
}
temp = i->next; //因為下文要刪除i結點,所以記錄i結點的下一個結點
list_del(i);
__list_add(i,j,j->next); //把i插入到j的後面
i = temp->prev; //i指標歸位
}
}
int main(void)
{
struct data_info s[] = {{6}, {4}, {7}, {9}, {2}, {8}, {5}, {1}, {3}, {7}};
LIST_HEAD(head);
int i;
for (i = 0; i < sizeof s/ sizeof *s; ++i)
{
list_add_tail(&s[i].list, &head);
} //尾插,構成連結串列
struct data_info *pdata = NULL;
list_for_each_entry(pdata, &head, list)
{
printf("%d ", pdata->data);
}
printf("\n"); //排序之前
insert_sort(&head, cmp_data); //進行排序
list_for_each_entry(pdata, &head, list)
{
printf("%d ", pdata->data);
}
printf("\n"); //排序之後
return 0;
}
以上程式碼執行結果如下:
6 4 7 9 2 8 5 1 3 7
1 2 3 4 5 6 7 7 8 9
三、程式碼解析
1. swap
函式
2. list_for_each_from
巨集
#define list_for_each_from(cur, head) \
for (; cur != head; cur = (cur)->next)
這個巨集表示從當前結點開始遍歷,一直到連結串列尾端。
3. list_for_each_reverse_continue
巨集
#define list_for_each_reverse_continue(cur, head) \
for (cur = (cur)->prev; cur != (head); cur = (cur)->prev)
這個巨集表示從當前結點的前一個結點開始,逆序遍歷,一直到第一個結點。
4. __list_add
函式
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
把new
指向的結點插入到prev
和next
結點之間。
5. insert_sort
函式
void insert_sort(struct list_head *head,
int(*cmp)(struct list_head *a,
struct list_head *b))
{
struct list_head *i, *j,*temp;
i = head->next->next; //i指向第2個結點
list_for_each_from(i,head){ //i從第2個結點開始遍歷,因為第1個已經有序
j = i->prev; //j指向i的前一個結點
if (cmp(j, i) <= 0) //從表頭開始,按照升序排列
continue;
list_for_each_reverse_continue(j,head){
if(cmp(j,i) <= 0)
break;
}
temp = i->next; //因為下文要刪除i結點,所以記錄i結點的下一個結點
list_del(i);
__list_add(i,j,j->next); //把i插入到j的後面
i = temp->prev; //i指標歸位
}
}
6~7行:i
從第二個結點開始,一直遍歷到最後一個結點;
第10行:j
指向i
結點的前驅,如果j
結點小於等於i
結點,說明i
不需要插入,它已經在合適的位置(不一定是最終位置)上了,此時進入下一輪迭代;
第12~14行:能執行到第12行,說明j
結點大於i
結點,這時候我們要做的是——從j
向前找,找到第一個小於等於i
的結點,這個結點用j
指示。找到後跳出這層迴圈。
17~18行:我們需要把i
結點插入到j
的後面。
16和19行:因為i
結點移動了,所以i
指標需要歸位,第16行記錄了i
結點的下一個結點,叫temp
,第19行讓i
指向temp的前驅,完成歸位。為什麼要歸位?可以參考我的博文 http://blog.csdn.net/longintchar/article/details/78638975 中的4.4節。
【完】