深入单链表的快速排序详解

时间:2022-01-01 04:08:35

单链表的快排序和数组的快排序基本思想相同,同样是基于划分,但是又有很大的不同:单链表不支持基于下标的访问。故书中把待排序的链表拆分为2个子链表。为了简单起见,选择链表的第一个节点作为基准,然后进行比较,比基准小得节点放入左面的子链表,比基准大的放入右边的子链表。在对待排序链表扫描一遍之后,左边子链表的节点值都小于基准的值,右边子链表的值都大于基准的值,然后把基准插入到链表中,并作为连接两个子链表的桥梁。然后分别对左、右两个子链表进行递归快速排序,以提高性能。
但是,由于单链表不能像数组那样随机存储,和数组的快排序相比较,还是有一些需要注意的细节:

1、支点的选取,由于不能随机访问第K个元素,因此每次选择支点时可以取待排序那部分链表的头指针。
2、遍历量表方式,由于不能从单链表的末尾向前遍历,因此使用两个指针分别向前向后遍历的策略实效,
事实上,可以可以采用一趟遍历的方式将较小的元素放到单链表的左边。
具体方法为:
1)定义两个指针pslow,pfast,其中pslow指向单链表的头结点,pfast指向单链表头结点的下一个结点;
2)使用pfast遍历单链表,每遇到一个比支点小的元素,就令pslow=pslow->next,然后和pslow进行数据交换。
3、交换数据方式,直接交换链表数据指针指向的部分,不必交换链表节点本身。
基于上述思想的单链表快速排序实现如下:

复制代码 代码如下:


/**  
** 单链表的快速排序  
** author :liuzhiwei
** date   :2011-08-07  
**/
#include<iostream>
#include<ctime>
using namespace std;
//单链表节点
struct SList
{
&nbsp;&nbsp; &nbsp;int data;
&nbsp;&nbsp; &nbsp;struct SList* next;
};
void bulid_slist(SList** phead, int n)&nbsp;&nbsp;&nbsp; //指向指针的指针
{
&nbsp;&nbsp; &nbsp;int i;
&nbsp;&nbsp; &nbsp;SList* ptr = *phead;
&nbsp;&nbsp; &nbsp;for(i = 0; i < n; ++i)
&nbsp;&nbsp; &nbsp;{
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;SList* temp = new SList;
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;temp->data = rand() % n;&nbsp;&nbsp; //产生n个n以内的随机数
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;temp->next = NULL;
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;if(ptr == NULL)
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;{
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;*phead = temp;
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;ptr = temp;
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;else
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;{
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;ptr->next = temp;
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;ptr = ptr->next;
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;}
}
void print_slist(SList* phead)&nbsp;&nbsp; //输出链表
{
&nbsp;&nbsp; &nbsp;SList *ptr = phead;
&nbsp;&nbsp; &nbsp;while(ptr)
&nbsp;&nbsp; &nbsp;{
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;printf("%d ", ptr->data);
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;ptr = ptr->next;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;printf("\n");
}
void my_swap(int *a,int *b)
{
&nbsp;&nbsp; &nbsp;int temp;
&nbsp;&nbsp; &nbsp;temp=*a;
&nbsp;&nbsp; &nbsp;*a=*b;
&nbsp;&nbsp; &nbsp;*b=temp;
}
void sort_slist(SList* phead, SList* pend)&nbsp;&nbsp;&nbsp; //将头指针为phead,尾指针为pend的链表进行排序
{
&nbsp;&nbsp; &nbsp;if(phead == NULL)
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;return ;
&nbsp;&nbsp; &nbsp;if(phead == pend)
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;return ;
&nbsp;&nbsp; &nbsp;SList *pslow = phead;
&nbsp;&nbsp; &nbsp;SList *pfast = phead->next;
&nbsp;&nbsp; &nbsp;SList *ptemp = phead;
&nbsp;&nbsp; &nbsp;while(pfast != pend)
&nbsp;&nbsp; &nbsp;{
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;if(pfast->data < phead->data)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //每次都选择待排序链表的头结点作为划分的基准
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;{
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;ptemp = pslow;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //ptemp始终为pslow的前驱结点
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;pslow = pslow->next;
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;my_swap(&pslow->data , &pfast->data);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //pslow指针指向比基准小的结点组成的链表
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;pfast = pfast->next;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;my_swap(&pslow->data , &phead->data);&nbsp; //此时pslow指针指向比基准小的结点组成的链表的最后一个结点,也就是基准的位置,所以要与基准(head结点)交换
&nbsp;&nbsp; &nbsp;sort_slist(phead , pslow);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //ptemp为左右两部分分割点(基准)的前一个结点
&nbsp;&nbsp; &nbsp;sort_slist(pslow->next , NULL);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //右部分是比基准大的结点组成的链表
}
void destroy_slist(SList* phead)
{
&nbsp;&nbsp; &nbsp;SList* ptr = phead;
&nbsp;&nbsp; &nbsp;while(ptr)
&nbsp;&nbsp; &nbsp;{
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;SList* temp = ptr;
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;ptr = ptr->next;
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;delete temp;
&nbsp;&nbsp; &nbsp;}
}
int main(void)
{
&nbsp;&nbsp; &nbsp;srand(time(NULL));
&nbsp;&nbsp; &nbsp;printf("Before sort single list\n");
&nbsp;&nbsp; &nbsp;SList* phead = NULL;
&nbsp;&nbsp; &nbsp;bulid_slist(&phead, 100);
&nbsp;&nbsp; &nbsp;print_slist(phead);
&nbsp;&nbsp; &nbsp;printf("After sort single list\n");
&nbsp;&nbsp; &nbsp;sort_slist(phead, NULL);
&nbsp;&nbsp; &nbsp;print_slist(phead);
&nbsp;&nbsp; &nbsp;destroy_slist(phead);
&nbsp;&nbsp; &nbsp;system("pause");
&nbsp;&nbsp; &nbsp;return 0;
}


第二种方法:
选择链表的第一个节点作为基准,然后进行比较,比基准小得节点放入左面的子链表,比基准大的放入右边的子链表。在对待排序链表扫描一遍之后,左面子链表的节点值都小于基准的值,右边子链表的值都大于基准的值,然后把基准插入到链表中,并作为连接两个子链表的桥梁。然后根据左、右子链表中节点数,选择较小的进行递归快速排序,而对数目较多的则进行迭代排序。
排序函数中使用的变量如下:

复制代码 代码如下:


 struct node *right;   //右边子链表的第一个节点
struct node **left_walk, **right_walk;    //作为指针,把其指向的节点加入到相应的子链表中
struct node *pivot, *old;    //pivot为基准, old为循环整个待排序链表的指针
核心代码如下:
for (old = (*head)->next; old != end; old = old->next) {
      if (old->data < pivot->data) {  //小于基准,加入到左面的子链表,继续比较
             ++left_count;
         *left_walk = old;            //把该节点加入到左边的链表中,
         left_walk = &(old->next);
} else {                      //大于基准,加入到右边的子链表,继续比较
         ++right_count;
             *right_walk = old;         
             right_walk = &(old->next);
      }
}


 head为struct node **类型,指向链表头部,end指向链表尾部,可为NULL,这段程序的重点在于指针的指针的用法,*left_walk为一个指向node节点的指针,说的明白点*left_walk的值就是node节点的内存地址,其实还有一个地方也有node的地址,那就是指向node的节点的next域,故我们可以简单的认为*left_walk = old就是把指向node节点的节点的next域改为节点old的地址,这样可能造成两种情况:一种就是*left_walk本来就指向old节点,这样就没有改变任何改变,另一种则是改变了*right_walk指向节点的前一个节点的next域,使其指向后部的节点,中间跳过了若干个节点,不过在这里这样做并不会造成任何问题,因为链表中的节点要么加入到左面的子链表中,要么加入到右面的子链表中,不会出现节点丢失的情况。
下面用图示说明下上面的问题:

深入单链表的快速排序详解


这里假设链表的值一次是5、2、4、6、1。根据程序首先head = left_walk指向值为5的节点,old指向值为2的节点,2小于5,所以加入2到左面的子链表中,*left_walk=old,我们知道,*left_walk指向的是第一个节点,这样做改变了head指针值,使其指向第二个节点,然后left_walk后移,old后移,4同样小于5,故继续上述操作,但是这是*left_walk和old指向的是同一个节点,没有引起任何变化,left_walk和old后移,6大于5,这时不同就出现了,要把其加入到右边的子链表中,故是*right_walk = old,其实right_walk初试化为&right,这句话相当于right = old,即令old当前指向的节点作为右边子链表的第一个节点,以后大于基准的节点都要加入到这个节点中,且总是加入到尾部。此时right_walk,和old后移,1小于5应该加入到左边的子链表中,*left_walk = old,此时*left_walk指向6,故此语句的作用是更改节点4的next值,把其改为1的地址,这样6就从原来的链表中脱钩了,继续left_walk和old后移到9节点,应加入到右边的子链表中,此时*right_walk指向1,故把9节点加入到6节点的后面。

这就是基本的排序过程,然而有一个问题需要搞明白,比如有节点依次为struct node *a, *b, *c,node **p , p = &b,如果此时令*p = c,即实际效果是a->next = c;我们知道这相当于该a的next域的值。而p仅仅是一个指针的指针,它是指向b所指向的节点的地址的指针,那么当我们更改*p的值的时候怎么会改到了a的next呢(这个可以写程序验证下,确实如此)?其实并非如此,我们仔细的看看程序,left_walk初始化为head,那么第一次执行*left_walk是把head指向了左边链表的起始节点,然后left_walk被赋值为&(old->next),这句话就有意思了,我们看一看下面在执行*left_walk=old时的情况,可以简单的来个等价替换,*left_walk = old也就相当于*&(old->next) = old,即old->nex = old,不过这里的old可不一定是old->next所指向的节点,应为left_walk和right_walk都指向它们的old节点,但是却是不同的。

算法到这里并没有完,这只是执行了一次划分,把基准放入了正确的位置,还要继续,不过下面的就比较简单了,就是递归排序个数比较小的子链表,迭代处理节点数目比较大的子链表。
完整的代码如下:

复制代码 代码如下:


#include <stdio.h>  
#include <stdlib.h>  
#include <time.h>  
//链表节点  
struct node
{
 int data;  
 struct node *next;  
};  
//链表快排序函数
void QListSort(struct node **head,struct node *h);  
//打印链表  
void print_list(struct node *head)
{
 struct node *p;  
 for (p = head; p != NULL; p = p->next)
 {  
  printf("%d ", p->data);  
 }  
 printf("\n");  
}  
int main(void)
{
 struct node *head = NULL;  
 struct node *p;  
 int i;  
 /** 
 * 初始化链表 
 */
 srand((unsigned)time(NULL));  
 for (i = 1; i < 11; ++i)
 {  
  p = (struct node*)malloc(sizeof(struct node));  
  p->data = rand() % 100 + 1;
  if(head == NULL)
  {
   head = p;
   head->next = NULL;
  }
  else
  {
   p->next = head->next;
   head->next = p;
  }
 }
 print_list(head);
 printf("---------------------------------\n");
 QListSort(&head,NULL);
 print_list(head);
 system("pause");
 return 0;  
}  
void QListSort(struct node **head, struct node *end)
{
 struct node *right;  
 struct node **left_walk, **right_walk;  
 struct node *pivot, *old;  
 int count, left_count, right_count;  
 if (*head == end)
  return;  
 do {  
  pivot = *head;  
  left_walk = head;
  right_walk = &right;  
  left_count = right_count = 0;  
  //取第一个节点作为比较的基准,小于基准的在左面的子链表中,  
  //大于基准的在右边的子链表中  
  for (old = (*head)->next; old != end; old = old->next)
  {  
   if (old->data < pivot->data)
   {      //小于基准,加入到左面的子链表,继续比较  
    ++left_count;  
    *left_walk = old;            //把该节点加入到左边的链表中,  
    left_walk = &(old->next);  
   }
   else
   {                         //大于基准,加入到右边的子链表,继续比较  
    ++right_count;  
    *right_walk = old;             
    right_walk = &(old->next);  
   }  
  }  
  //合并链表  
  *right_walk = end;       //结束右链表  
  *left_walk = pivot;      //把基准置于正确的位置上  
  pivot->next = right;     //把链表合并  
  //对较小的子链表进行快排序,较大的子链表进行迭代排序。  
  if(left_walk > right_walk)
  {
   QListSort(&(pivot->next), end);  
   end = pivot;  
   count = left_count;  
  }
  else
  {  
   QListSort(head, pivot);  
   head = &(pivot->next);  
   count = right_count;  
  }  
 }
 while (count > 1);   
}