1. 程式人生 > >雙向迴圈連結串列的插入排序

雙向迴圈連結串列的插入排序

前兩篇博文,我討論了連結串列的氣泡排序和選擇排序(以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指向的結點插入到prevnext結點之間。

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節。

【完】