排序-交换排序-快速排序

快速排序

快速排序Quick Sort)是由冒泡排序改进而得的。在冒泡排序过程中,只对相邻的两个记录进行比较,因此每次交换两个相邻记录时只能消除一个逆序。如果能通过两个(部相邻)记录的一次交换,消除多个逆序,则会大大加速排序的速度。快速排序方法中的一次交换可能消除多个逆序。

算法思路

[算法步骤]

在待排序表L[1…n]中任取一个元素作为枢轴(或支点,基准,通常去第一个元素),设其关键字为pivotKey, 经过一趟排序后,把所有关键字小于pivotKey的记录交换到前面,把所有关键字大于pivotKey的记录交换到后面。结果将待排序记录表分成两个子表,最后将枢轴放置到分界处位置。然后,分别对左、右子表进行快速排序,直到每个子表只有一个记录时,排序完成。

其中,一趟快排的具体步骤如下:
① 选择待排序表中的第一个记录作为枢轴,将枢轴记录暂存在r[0]的位置上。附设两个指针low和high,初始分别指向表的下界和上界(第一趟时,low=0,high=L.length)。
② 从表的最右侧位置依次向左搜索,找到第一个关键字小于枢轴关键字pivotKey的记录,将其左移到low处。
具体操作是:当low<high时,若high所指记录的关键字<=pivotKey,则向左移动指针high(high–);否则将high所指记录与枢轴记录交换。
③ 然后在从表的最左侧位置,依次向右搜索找到第一个关键字<=pivotKey,则向右移动low(lwo++);否则low所指记录与枢轴记录交换。
④ 重复步骤②和③,直到low和high相等为止。此时lwo或high的位置即为枢轴在此趟排序中的最终位置,原表被分成两个子表。

在上述过程中,记录的交换都是与枢轴之间发生,每次交换都要移动3次记录,可以先将枢轴暂存在r[0]的位置上,排序过程中只移动要与枢轴交换的记录,即只做r[low]或r[high]的单向移动, 直到一趟排序结束后在将枢轴记录移动至正确位置上。

算法实现

定义存放记录的线性表结构

//记录
typedef struct ElemType{
    
    
    int key;
    string info;
}ElemType;

#define InitSize 50
typedef struct {
    
    
    ElemType data[InitSize];
    int length;
}SqList;

void toString(SqList L);

void toString(SqList L,int low,int high);

void InitSqList(SqList &L);

代码实现

int amount = 0;
void QuickSort(SqList &L, int low, int high) {
    
    
//  初始调用 low = 1;high=L.length;
    cout << "-------------------------第  " << ++amount << "次趟排序,low="<<low<<",high="<<high <<"-------------------------------"<< endl;
    toString(L,low,high);
    if (low < high) {
    
     //子表长度大于1
        int pivotLoc = partition(L, low, high); //将L.r[low...high]一分为二,pivotLoc时枢轴的位置
        cout << "当前结果:"<< endl;
        toString(L);
        QuickSort(L, low, pivotLoc - 1); //对左子表递归排序
        QuickSort(L, pivotLoc + 1, high);//对右子表递归排序
    }
}
int partition(SqList &L, int low, int high) {
    
    
    L.data[0] = L.data[low]; //用子表的第一个记录做枢轴记录
    int pivotKey = L.data[0].key;
    cout << " 当前枢轴key=  " << pivotKey << endl;
    while (low < high) {
    
    
        while (low < high && L.data[high].key >= pivotKey) --high; //从右向左,找到第一个>=pivotKey的记录
        L.data[low] = L.data[high]; //比枢轴小的记录移动到低位
        cout << " r[" << high <<"].key("<<L.data[high].key<<")>= pivotKey(" << pivotKey << ") 移动到低位 *" << low << "\t";
        toString(L);
        while (low < high && L.data[low].key <= pivotKey) ++low;//从左向右,找到第一个<=pivotKey的记录
        L.data[high] = L.data[low];//比枢轴大的记录移动到高位
        cout << " r[" << low << "].key("<<L.data[low].key<<")<= pivotKey(" << pivotKey << ") 移动到高位 *" << high << "\t";
        toString(L);
    }
    L.data[low] = L.data[0]; //将枢轴放置到正确的位置
    cout << " 枢轴的正确位置:" << low << endl;
    return low;
}

测试案例

void testQuickSort() {
    
    
    SqList L;
    InitSqList(L);
    cout <<" ----------------------------------初始 L---------------------------------------- "<< endl;
    toString(L);
    QuickSort(L,1,L.length);
    cout <<" --------------------------------最终排序结果--------------------------------------- "<< endl;
    toString(L,1,L.length);
}
int main() {
    
    
    testQuickSort();
    return 0;
}

测试结果

 ----------------------------------初始 L---------------------------------------- 
key  [ 0 41 67 34 0 69 24 78 58 62 64 ]
-------------------------1次趟排序,low=1,high=10-------------------------------
待排序子表 [ 41 67 34 0 69 24 78 58 62 64 ]
 当前枢轴key=  41
 r[6].key(24)>= pivotKey(41) 移动到低位 *1	key  [ 41 24 67 34 0 69 24 78 58 62 64 ]
 r[2].key(67)<= pivotKey(41) 移动到高位 *6	key  [ 41 24 67 34 0 69 67 78 58 62 64 ]
 r[4].key(0)>= pivotKey(41) 移动到低位 *2	key  [ 41 24 0 34 0 69 67 78 58 62 64 ]
 r[4].key(0)<= pivotKey(41) 移动到高位 *4	key  [ 41 24 0 34 0 69 67 78 58 62 64 ]
 枢轴的正确位置:4
当前结果:
key  [ 41 24 0 34 41 69 67 78 58 62 64 ]
-------------------------2次趟排序,low=1,high=3-------------------------------
待排序子表 [ 24 0 34 ]
 当前枢轴key=  24
 r[2].key(0)>= pivotKey(24) 移动到低位 *1	key  [ 24 0 0 34 41 69 67 78 58 62 64 ]
 r[2].key(0)<= pivotKey(24) 移动到高位 *2	key  [ 24 0 0 34 41 69 67 78 58 62 64 ]
 枢轴的正确位置:2
当前结果:
key  [ 24 0 24 34 41 69 67 78 58 62 64 ]
-------------------------3次趟排序,low=1,high=1-------------------------------
待排序子表 [ 0 ]
-------------------------4次趟排序,low=3,high=3-------------------------------
待排序子表 [ 34 ]
-------------------------5次趟排序,low=5,high=10-------------------------------
待排序子表 [ 69 67 78 58 62 64 ]
 当前枢轴key=  69
 r[10].key(64)>= pivotKey(69) 移动到低位 *5	key  [ 69 0 24 34 41 64 67 78 58 62 64 ]
 r[7].key(78)<= pivotKey(69) 移动到高位 *10	key  [ 69 0 24 34 41 64 67 78 58 62 78 ]
 r[9].key(62)>= pivotKey(69) 移动到低位 *7	key  [ 69 0 24 34 41 64 67 62 58 62 78 ]
 r[9].key(62)<= pivotKey(69) 移动到高位 *9	key  [ 69 0 24 34 41 64 67 62 58 62 78 ]
 枢轴的正确位置:9
当前结果:
key  [ 69 0 24 34 41 64 67 62 58 69 78 ]
-------------------------6次趟排序,low=5,high=8-------------------------------
待排序子表 [ 64 67 62 58 ]
 当前枢轴key=  64
 r[8].key(58)>= pivotKey(64) 移动到低位 *5	key  [ 64 0 24 34 41 58 67 62 58 69 78 ]
 r[6].key(67)<= pivotKey(64) 移动到高位 *8	key  [ 64 0 24 34 41 58 67 62 67 69 78 ]
 r[7].key(62)>= pivotKey(64) 移动到低位 *6	key  [ 64 0 24 34 41 58 62 62 67 69 78 ]
 r[7].key(62)<= pivotKey(64) 移动到高位 *7	key  [ 64 0 24 34 41 58 62 62 67 69 78 ]
 枢轴的正确位置:7
当前结果:
key  [ 64 0 24 34 41 58 62 64 67 69 78 ]
-------------------------7次趟排序,low=5,high=6-------------------------------
待排序子表 [ 58 62 ]
 当前枢轴key=  58
 r[5].key(58)>= pivotKey(58) 移动到低位 *5	key  [ 58 0 24 34 41 58 62 64 67 69 78 ]
 r[5].key(58)<= pivotKey(58) 移动到高位 *5	key  [ 58 0 24 34 41 58 62 64 67 69 78 ]
 枢轴的正确位置:5
当前结果:
key  [ 58 0 24 34 41 58 62 64 67 69 78 ]
-------------------------8次趟排序,low=5,high=4-------------------------------
待排序子表 [ ]
-------------------------9次趟排序,low=6,high=6-------------------------------
待排序子表 [ 62 ]
-------------------------10次趟排序,low=8,high=8-------------------------------
待排序子表 [ 67 ]
-------------------------11次趟排序,low=10,high=10-------------------------------
待排序子表 [ 78 ]
 --------------------------------最终排序结果--------------------------------------- 
待排序子表 [ 0 24 34 41 58 62 64 67 69 78 ]

进程已结束,退出代码0

算法分析

① 时间复杂度:

从快速排序算法的递归树可知,快速排序的趟数取决于递归树的深度。

  • 最好情况:

每一趟排序后都能将记录均匀的分割成两个长度大致相等的子表,类似折半查找。在n个元素的序列中,对枢轴定位所需时间为O(n)。若设T(n)是对n个元素的序列进行排序所需的时间,而且每次对枢轴正确定位后,正好把序列划分为长度相等的两个子表,此时,设Cn是一个常数,表示n个元素进行一趟快速排序的时间,则总的排序时间为

    T(n) = Cn +2T(n/2)
          <= n+2T(n/2)
          <= n+2T(n/2+2T(n/4)) = 2n+4T(n/4)
          <= 2n+4(n/4+2T(n/8)) = 3n+8T(n/8)
          ....
          <= kn+2^kT(n/2^k)
          ∵ k=log2n
          ∴ T(n)≤ nlog2n +nT(1) ≈ O(nlog2n)
  • 最坏情况:

待排序序列已经排好序的情况下,其递归树成为单支树,每次划分只得到一个比上一次少一个记录的子序列。
这样,必须经过n-1趟才能将所有的记录定位,而且第i趟需要经过n-i次比较。这样,总的关键字比较次数KCN为

KCN=(n-1)+(n-2)+....+(3-1)+(2-1) 
   =n(n-1)/2 ≈ n^2/2

这种情况下,快速排序的速度已经蜕化到简单排序的水平。枢轴的合理选择可以避免这种最坏情况的出现,如利用”三者取中“的规则:比较当前表中第一个记录、最后一个记录和中间一个记录的关键字,去关键字居中的记录作为枢轴记录,事先调换到第一个记录的位置。
理论上可以证明,平均情况下,快速排序的时间复杂度为O(nlog2n)

② 空间复杂度

快速排序是递归的,执行时需要一个栈来存放相应的数据。最大递归调用次数与递归树的深度一致,所以最好情况下的空间复杂度为O(nlog2n),最坏情况下为O(n).

[算法特点]

① 记录非顺次的移动导致排序方法是不稳定的
② 排序过程中需要定位表的下界和上界,所以适合用于顺序结构,很难用于链式结构
③ 当n较大时,在平均情况下快速排序是所有内部排序方法中速度最快的一种,所以其适合初始记录无序、n较大时的情况。

猜你喜欢

转载自blog.csdn.net/QQ657205470/article/details/127747109