Sir zach

专注于算法,AI, Android以及音视频领域 欢迎关注我的最新博客: zachliu.cn

导航

快速排序实现

Posted on 2014-08-30 21:01  SirZach  阅读(258)  评论(0编辑  收藏  举报

  作为统治世界的算法之一,快速排序(Quick Sort)在很多场合下都能发挥其强大的力量。数据量在百万级别的数据量对快速排序来说是小case.  该算法最早是由图灵奖获得者Tony Hoare设计出来的,他在形式化方法理论以及ALGOL60编程语言的发明中都有卓越的贡献。可以认为是冒泡排序的升级,它们都属于交换排序类。即通过不断的比较和移动交换来实现排序,只不过它的实现,增大了记录的比较和移动的距离,将关键字较大的记录移到后面,较小的移到前面,从而减少了总的比较次数和移动交换次数。

  在实现快速排序的过程中,我们一般需要关注2点,一个是枢纽元的选择(即pivot),  还有很重要的一点是两个工作指针的初始位置以及他们的运动方向。

  看了很多的实现,pivot一般来说有4种选择:

  1、头元素        Hoare版本的做法,《数据结构与算法分析》中不推荐将第一个元素作为枢纽元,因为在输入是预排序或反序时,会产生糟糕的分割。 其实尾元素道理是一样的。

  2、尾元素        Lomuto版本的做法

  3、中间元素(包括3数中值分割法(median of three), 即取3个关键字先进行排序,将中间数作为pivot, 一般取左中右3个数,也可以随机选取。)

  4、随机选择   排除随机数生成的代价外,是一种不错的选择

 

快排一般来说有以下两个版本,

  一、先看看Hoare最原始的版本,pivot为首元素,其工作指针分别在一头一尾,这完全就一稍微快一点的冒泡嘛= =:

image

具体实现如下:

#include <stdio.h>

void swap(int *a, int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

void printArray(int a[], int num)
{
    for (int i = 0; i < num; i++)
    {
         printf("%d ", a[i]);
    }
     printf("\n");
}

int HoarePartition(int a[], int p, int r)
{
    int key = a[p], i = p - 1, j = r + 1;
    
     // fprintf(fp, "key = %d, i = %d, j = %d\n", key, i, j);

    while (1)
    {
        do 
        {
            j--;
        }while (a[j] > key);
        do
        {
            i++;
        }while (a[i] < key);

        if (i < j)
        {
            swap(&a[i], &a[j]);
        }
        else
        {
            return j;
        }
    }

}

void QuickSort(int a[], int start, int end)
{
    int q;
     // fprintf(fp, "new sort: start = %d, end = %d\n", start, end);

    if (end <= start)
        return;

    q = HoarePartition(fp, a, start, end);
    QuickSort(fp, a, start, q);
    QuickSort(fp, a, q + 1, end);
}

int main()
{
    int a[] = {3, 4, 12, 56, 0, 6, 9, 10, 6, 23};
 
    printf(fp, "init array: \n");
    printArray(fp, a, 10);

    QuickSort(fp, a, 0, 9);
    printf(fp, "sorted: \n");
    printArray(fp, a, 10);
            
    fclose(fp);
    return 0;
}

二、Nico Lomuto也提出了一个版本,pivot为尾元素,工作指针都从左向右一个方向运动,个人觉得这个更好理解:

Lomuto-Partition(A, p, r)
x = A[r]
i = p - 1
for j = p to r - 1
    if A[j] <= x
        i = i + 1
        swap( A[i], A[j] )
swap( A[i+1], A[r] )
return i + 1

QUICKSORT(A, p, r)
 if p < r
    then q ← Lomuto-Partition(A, p, r)   
         QUICKSORT(A, p, q - 1)
         QUICKSORT(A, q + 1, r)

分析如下,摘自算法导论:

image

两种实现方式对比的话, Lomuto版本更加简单易实现,但不适用于库函数的实现,因为它使用了更多的交换次数。更加细节参见:StackExchange

 

下面是Lomuto版本的实现代码:

#include <stdio.h>

void printArray(int a[], int num)
{
    for (int i = 0; i < num; i++)
    {
        printf("%d ", a[i]);
    }
    printf("\n");
}

void swap(int *a, int *b)
{
    int t;
    t = *a;
    *a = *b;
    *b = t;
}


int Partition(int data[], int p, int r)
{
    int key = data[r];
    int i = p - 1;

    for (int j = p; j < r; j++)
    {
        if (data[j] <= key)
        {
            i++;
            swap(&data[i], &data[j]);
        }
    }
    swap(&data[i+1], &data[r]);

    return i + 1;
}

void QuickSort(int data[], int start, int end)
{
    if (start < end)
    {
        int q = Partition(data, start, end);
        QuickSort(data, start, q - 1);
        QuickSort(data, q + 1, end);
    }

}

int main()
{
    int a[] = {3, 32, 13, 8, 9, 9, 12, 33, 41};

    printf("init array:\n");
    printArray(a, 9);
    QuickSort(a, 0, 8);
    printf("sorted:\n");
    printArray(a, 9);
}

 

下面讨论一下变种版本:

三、一种Hoare变种实现,pivot为首元素,工作指针从两边向中间移动,也比较好理解

#include <stdio.h>

void swap(int *a, int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

void printArray(int a[], int num)
{
    for (int i = 0; i < num; i++)
    {
        printf("%d ", a[i]);
    }
    printf("\n");
}

int HoarePartition(int a[], int p, int r)
{
    int key = a[p], i = p, j = r;
    
    // fprintf(fp, "key = %d, i = %d, j = %d\n", key, i, j);

    while (i < j)
    {
        while (a[j] >= key && i < j)
            j--;
        a[i] = a[j];

        while (a[i] <= key && i < j)
            i++;
        a[j] = a[i];
    }

    a[i] = key;
    return i;
}

void QuickSort(int a[], int start, int end)
{
    int q;
    // fprintf(fp, "new sort: start = %d, end = %d\n", start, end);

    if (end <= start)
        return;

    q = HoarePartition(a, start, end);
    QuickSort(a, start, q - 1);
    QuickSort(a, q + 1, end);
}

int main()
{
    int a[] = {3, 4, 12, 56, 0, 6, 9, 10, 9, 23};
 
     // FILE *fp = fopen("log", "w+");

    printf("init array: \n");
    printArray(a, 10);
    printf("sorted: \n");
    QuickSort(a, 0, 9);
    printArray(a, 10);

            
    fclose(fp);
    return 0;
}
从上面的实现可以看出,快速排序用到的都是递归实现,这在数据量较大的情况下容易出现堆栈溢出,我在一次测试中使用10万的数据量出现了溢出(有点疑惑不过程序确认崩溃了),
所以当你的需求是比较大的数据量时,非递归的实现就会派上用场了。这里看到有人用STL stack实现发现速度明显比较慢(快速排序算法的几种实现的性能对比:递归实现和非递归实现),所以还是建议自己实现堆栈比较靠谱:)
#include <stdio.h>

void swap(int *a, int *b)
{

    int temp;

    temp = *a;
    *a = *b;
    *b = temp;
}

int partition(int *a, int start, int end)
{
    int key = a[end];
    int i = start - 1;
    int j = start;

    for ( ; j < end; j++)
    {
        if (a[j] < key)
        {
            i++;
            swap(&a[i], &a[j]);
        }
    }
    swap(&a[++i], &a[j]);

    return i;
}

void QuickSort(int *a, int size)
{
    int stack[100];
    int top = -1;
    int start, end;
    stack[++top] = 0;
    stack[++top] = size - 1;

    while (top > 0)
    {
        end = stack[top--];
        start = stack[top--];

        int i = partition(a, start, end);

        if (start < i - 1)
        {
            stack[++top] = start;
            stack[++top] = i - 1;
        }
        if (i + 1 < end)
        {
            stack[++top] = i + 1;
            stack[++top] = end;
        }
    }
}

void printArray(int *a, int num)
{
    for (int i = 0; i < num; i++)
    {
        printf("%d ", a[i]);
    }

    printf("\n");
}

int main()
{
    int a[] = {3, 45, 15, 6, 9, 15, 90, 17, 28, 10};

    printf("init array:\n");
    printArray(a, 10);

    QuickSort(a, 10);

    printf("after sorted:\n");
    printArray(a, 10);
    return 0;
}

那么单链表可以使用快速排序吗?答案是可以,实现思路是使用两个链表,一个保存比key小的值,另一个保存比key大的,最后把两个链表再连接起来。这样通过调整指针的指向即可实现排序效果。

#include <stdio.h>
#include <time.h>

typedef struct tagLinkNode
{
    int value;
    struct tagLinkNode *next;
}LinkNode;


void QuickSort(LinkNode** head, LinkNode** end)
{
    LinkNode *head1, *head2, *end1, *end2;
    head1 = head2 = end1 = end2 = NULL;

    if (*head == NULL || *end == NULL)
    {
        // printf("head or end is null\n");
        return;
    }
    // printf("head=%d, end=%d\n", (*head)->value, (*end)->value);

    LinkNode *p, *pre1, *pre2;
    p = pre1 = pre2 = NULL;

    int key = (*head)->value;
    // printf("key is %d\n", key);

    // divide head node
    p = (*head)->next;
    (*head)->next = NULL;

    while (p != NULL)
    {
        // value less than key
        if (p->value < key)
        {
            // printf("less than key!\n");
            if (!head1)
            {
                head1 = p;
                pre1 = p;
            }
            else
            {
                pre1->next = p;
                pre1 = p;
            }
            p = p->next;
            pre1->next = NULL;
        }
        // value larger than key
        else
        {
            // printf("larger than key!\n");
            if (!head2)
            {
                head2 = p;
                pre2 = p;    
            }
            else
            {
                pre2->next = p;
                pre2 = p;
            }
            p = p->next;
            pre2->next = NULL;
        }

    }    

    // printf("merge it\n");
    end1 = pre1;
    end2 = pre2;

    // recuring
    QuickSort(&head1, &end1);
    QuickSort(&head2, &end2);

    // printf("after recuring\n");
    /* conjection 2 List */
    // if 2 List all exist
    if (head1 && head2)
    {
        end1->next = *head;
        (*head)->next = head2;
        *head = head1;
        *end = end2;
    }
    // only left list exist
    else if (head1)
    {
        end1->next = *head;
        *end = *head;
        *head = head1;
    }
    // only right list exist
    else if (head2)
    {
        (*head)->next = head2;
        *end = end2;
    }
}

void addList(LinkNode **head, LinkNode *node)
{
    node->value = rand() % 50 + 1;
    node->next = (*head)->next;

    (*head)->next = node;

    printf("%d ", node->value);
}

LinkNode* getListFirst(LinkNode *head)
{
    return head->next;
}

LinkNode* getListLast(LinkNode *head)
{
    LinkNode *p = head;
    while (p->next != NULL)
    {
        p = p->next;
    }

    return p;
}

void printList(LinkNode *head)
{
    LinkNode *p = head->next;

    while (p != NULL)
    {
        printf("%d ", p->value);
        p = p->next;
    }
}

int main()
{
    srand(time(NULL));
    int i;
    LinkNode linkArray[10];

    LinkNode *listHead;
    listHead = (LinkNode *) malloc(sizeof(LinkNode));
    if (NULL == listHead)
    {
        printf("listHead malloc failed\n");
        return -1;
    }
    listHead->value = 10;
    listHead->next = NULL;

    printf("init array: \n");
    for (i = 0; i < 10; i++)
    {
        addList(&listHead, &linkArray[i]);
    }
    printf("\n");

    LinkNode *first, *end;
    first = getListFirst(listHead);
    end = getListLast(listHead);
    QuickSort(&first, &end);
    listHead->next = first;   // important

    printf("after sorted: \n");
    printList(listHead);
    printf("\n");
}
总结:快速排序是一种平均复杂度在O(nlogn)的算法,最坏在已有序的情况下为O(n²),是不稳定的排序算法。

 

这里有一篇快排细节优化的文章,对pivot的选取以及存在相同元素的情况做了详述。