数据结构 | 第3章 列表(下)

125 阅读6分钟

3.4 有序列表

若列表中所有节点的逻辑次序与其大小次序完全一致,则称作有序列表(sorted list)。

  • 唯一化

    List::uniquify()

     0001 template <typename T> int List<T>::uniquify() { //成批剔除重复元素,效率更高
     0002    if ( _size < 2 ) return 0; //平凡列表自然无重复
     0003    int oldSize = _size; //记录原规模
     0004    ListNodePosi<T> p = first(); ListNodePosi<T> q; //p为各区段起点,q为其后继
     0005    while ( trailer != ( q = p->succ ) ) //反复考查紧邻的节点对(p, q)
     0006       if ( p->data != q->data ) p = q; //若互异,则转向下一区段
     0007       else remove ( q ); //否则(雷同),删除后者
     0008    return oldSize - _size; //列表规模变化量,即被删除元素总数
     0009 }
    

    复杂度:O(n)

  • 查找

    List::search()

     0001 template <typename T> //在有序列表内节点p(可能是trailer)的n个(真)前驱中,找到不大于e的最后者
     0002 ListNodePosi<T> List<T>::search ( T const& e, int n, ListNodePosi<T> p ) const {
     0003 // assert: 0 <= n <= rank(p) < _size
     0004    do {
     0005       p = p->pred; n--;  //从右向左
     0006    } while ( ( -1 < n ) && ( e < p->data ) ); //逐个比较,直至命中或越界
     0007    return p; //返回查找终止的位置
     0008 } //失败时,返回区间左边界的前驱(可能是header)——调用者可通过valid()判断成功与否
    

    尽管有序列表中的节点已在逻辑上按次序单调排列,但在动态存储策略中,节点的物理地址与逻辑次序毫无关系,故无法像有序向量那样自如地应用减治策略,从而不得不继续沿用无序列表的顺序查找策略。

    复杂度:O(n),而有序向量O(logn)

3.5 排序器

3.5.1 统一入口

 0001 template <typename T> void List<T>::sort ( ListNodePosi<T> p, int n ) { //列表区间排序
 0002    switch ( rand() % 4 ) { //随机选取排序算法。可根据具体问题的特点灵活选取或扩充
 0003       case 1:  insertionSort ( p, n ); break; //插入排序
 0004       case 2:  selectionSort ( p, n ); break; //选择排序
 0005       default: mergeSort ( p, n ); break; //归并排序
 0006    }
 0009 }

3.5.2 插入排序

有序的前缀,无序的后缀

image-20220623194939371.png

算法不变性:在任何时刻,相对于当前节点e = S[r],前缀S[0, r)总是已有序。

算法稳定性:多个元素命中时search()接口返回其中最靠后者,排序后重复元素保持其原有次序,故属于稳定算法。

 0001 template <typename T> //对列表中起始于位置p、宽度为n的区间做插入排序
 0002 void List<T>::insertionSort ( ListNodePosi<T> p, int n ) { //valid(p) && rank(p) + n <= size
 0003    for ( int r = 0; r < n; r++ ) { //逐一为各节点
 0004       insert ( search ( p->data, r, p ), p->data ); //查找适当的位置并插入
 0005       p = p->succ; remove ( p->pred ); //转向下一节点
 0006    }
 0007 }

复杂度O(n2)O(n^2)

3.5.3 选择排序

无序的前缀,有序的后缀

image-20220623195504092.png

算法不变性:在任何时刻,后缀S[r, n)已经有序,且不小于前缀S[0, r)。

算法稳定性:selectMax()接口可以定位最靠后的最大节点,排序后重复元素保持其原有次序,故属于稳定算法。

 0001 template <typename T> //对列表中起始于位置p、宽度为n的区间做选择排序
 0002 void List<T>::selectionSort ( ListNodePosi<T> p, int n ) { //valid(p) && rank(p) + n <= size
 0003    ListNodePosi<T> head = p->pred, tail = p;
 0004    for ( int i = 0; i < n; i++ ) tail = tail->succ; //待排序区间为(head, tail)
 0005    while ( 1 < n ) { //在至少还剩两个节点之前,在待排序区间内
 0006       ListNodePosi<T> max = selectMax ( head->succ, n ); //找出最大者(歧义时后者优先)
 0007       insert ( remove ( max ), tail ); //将其移至无序区间末尾(作为有序区间新的首元素)
 0008       tail = tail->pred; n--;
 0009    }
 0010 }

selectMax()接口:

 0001 template <typename T> //从起始于位置p的n个元素中选出最大者
 0002 ListNodePosi<T> List<T>::selectMax ( ListNodePosi<T> p, int n ) {
 0003    ListNodePosi<T> max = p; //最大者暂定为首节点p
 0004    for ( ListNodePosi<T> cur = p; 1 < n; n-- ) //从首节点p出发,将后续节点逐一与max比较
 0005       if ( !lt ( ( cur = cur->succ )->data, max->data ) ) //若当前元素不小于max,则
 0006          max = cur; //更新最大元素位置记录
 0007    return max; //返回最大节点位置
 0008 }

复杂度:O(n2)O(n^2),但由于选择排序属于CBA式算法,下界Ω(nlogn),故可优化。第10章会介绍借助更高级的数据结构,令单次selectMax()操作的复杂度降至O(logn),故整体效率提高到O(nlogn)。

3.5.4 归并排序

思想:先分解再归并

归并排序算法mergesort():

 0001 template <typename T> //列表的归并排序算法:对起始于位置p的n个元素排序
 0002 void List<T>::mergeSort ( ListNodePosi<T> & p, int n ) { //valid(p) && rank(p) + n <= size
 0003    if ( n < 2 ) return; //若待排序范围已足够小,则直接返回;否则...
 0004    int m = n >> 1; //以中点为界
 0005    ListNodePosi<T> q = p; for (int i = 0; i < m; i++) q = q->succ; //找到后子列表起点
 0006    mergeSort(p, m); mergeSort(q, n - m); //前、后子列表各分别排序
 0007    p = merge ( p, m, *this, q, n - m ); //归并
 0008 } //注意:排序后,p依然指向归并后区间的(新)起点

二路归并的一种实现:

 0001 template <typename T> //有序列表的归并:当前列表中自p起的n个元素,与列表L中自q起的m个元素归并
 0002 ListNodePosi<T> List<T>::merge ( ListNodePosi<T> p, int n, List<T> & L, ListNodePosi<T> q, int m ) {
 0003 // assert:  this.valid(p) && rank(p) + n <= size && this.sorted(p, n)
 0004 //          L.valid(q) && rank(q) + m <= L._size && L.sorted(q, m)
 0005 // 注意:在被mergeSort()调用时,this == &L && rank(p) + n = rank(q)
 0006    ListNodePosi<T> pp = p->pred; //归并之后p可能不再指向首节点,故需先记忆,以便在返回前更新
 0007    while ( ( 0 < m ) && ( q != p ) ) //q尚未出界(或在mergeSort()中,p亦尚未出界)之前
 0008       if ( ( 0 < n ) && ( p->data <= q->data ) ) //若p尚未出界且v(p) <= v(q),则
 0009          { p = p->succ; n--; } //p直接后移,即完成归入
 0010       else //否则,将q转移至p之前,以完成归入
 0011          { insert ( L.remove ( ( q = q->succ )->pred ), p ); m--; }
 0012    return pp->succ; //更新的首节点
 0013 }

复杂度:O(nlogn)

注意:

  1. 在子序列的划分阶段,向量与列表归并排序算法之间存在细微但本质的区别。前者支持循秩访问的方式,故可在O(1)时间内确定切分中点;后者仅支持循位置访问的方式,故不得不为此花费O(n)时间。幸好在有序子序列的合并阶段二者均需O(n)时间,故二者的渐进时间复杂度依然相等。
  2. 尽管二路归并算法并未对子列表的长度做出任何限制,但这里出于整体效率的考虑,在划分子列表时宁可花费O(n)时间使得二者尽可能接近于等长。反之,若为省略这部分时间而不保证划分的均衡性,则反而可能导致整体效率的下降。

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情