【数据结构】—— 列表 / 链表

     自学 邓俊辉《数据结构(C++语言版)》第三章——列表,且结合Leetcode题库进行简单练习。

Leetcode习题分类可参考此github

😊 😄 😆 😏 😭 😂 😍 🔥

以上github中链表练习题及代码总结点这里

     《数据结构(C++语言版)》电子版pdf获取方式:

     公众号【OAOA】回复【数据结构】即可。

1 向量、列表与链表

1.1 向量vector

     数据空间整体创建或销毁,数据元素的物理存储次序与其逻辑次序严格一致,可支持高效的静态操作;
     向量支持循秩访问(call-by-rank)的方式,根据数据元素的秩,可以在o(1)时间内直接确定其物理地址;

1.2 列表

     列表为各元素动态地分配和回收物理地址,在逻辑上相邻的元素记录彼此的物理地址,在逻辑上形成一个整体,支持高效的动态操作,支持循位置访问(call-by-position)的方式。

1.3 列表是链表的一般化推广

2 ListNode模板类

// ListNode模板类,即为List数据结构对象中的每一个元素
typedef int rank;
#define ListNodePosi(T) ListNode<T>* //列表节点位置

template <typename T>
struct ListNode
{
    /* data */
    T data; ListNodePosi(T) pred; ListNodePosi(T) succ;
    ListNode();
    ListNode(T e, ListNodePosi(T) p = NULL, ListNodePosi(T) s = NULL): data(e), pred(p), succ(s){}
    ListNodePosi(T) insertAsPred(T const& e);
    ListNodePosi(T) insertAsSucc(T const& e);
};

3 List对象

     如上图中,一个List对象的头节点(header)和尾节点(trailer)始终存在,但对外并不可见,它们是类的私有变量,这两个节点被称为哨兵节点。

那么这个哨兵节点有什么作用呢?

     引用哨兵节点能简化算法的描述与实现,避免对各种分界退化情况做专门处理。在下面的插入、删除操作中可以体现它的作用。

     创建一个List对象时,在列表内部创建一对头、尾哨兵节点,并设置其前驱、后继指针构成一个双向链表。

void List<T>::init(){
    header = new ListNode<T>;
    trailer = new ListNode<T>;
    header->succ=trailer; header->pred=NULL;
    trailer->succ=NULL; header->pred=header;
    _size = 0;
}

4 无序列表

     由秩到位置的转换:通过秩来指定列节点,可通过重载操作符“[]”:

查找操作:

插入操作:

删除操作:

     以上的插入与删除操作可以理解为是微创手术型的,整个过程仅仅牵扯到局部的三个节点,操作均可在常数时间内完成o(1)。
无序列表的唯一化:deduplicate()

Hints:

// 插入节点
ListNodePosi(T) ListNode<T>::insertAsPred(T const& e){
    ListNodePosi(T) x = new ListNode(e, pred, this);
    pred->succ = x;
    pred = x;
    return x;
}

     其中的两条语句 pred->succ = x; pred = x; 不能颠倒顺序!!!否则不能成功插入节点,原列表的结构会被破坏。

5 有序列表

     有序列表的唯一化:uniquify()

查找操作:

     有序列表的查找与无序列表的查找算法几乎一样,究其原因在于,尽管有序列表中的节点已在逻辑上按次序排列,但在动态存储策略中,节点的物理地址与逻辑毫无关系,无法像有序向量那样应用减治策略,只可以沿用顺序查找策略。

6 列表排序

6.1 插入排序 insertion sort

     插入排序 insertion sort适用于包括向量与列表在内的任何序列结构。
     思路:始终将整个序列视作并切分为两部分,有序的前缀 s[0, r) 和无序的后缀 S[r, n);通过迭代,反复地将后缀的首元素转移到前缀中。

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

// 插入排序
template <typename T> //列表的插入排序:对起始于位置 p 的 n 个元素排序
void List<T>::insertionSort( ListNodePosi(T) p, int n) { //valid(p) && rank(p) + n <= size
    for (int r=0; r<n; r++) { // r为已经排序的那个前缀的长度
        // search(e, r, p) 返回 p 的 r 个真前驱中不大于 e 的最后者位置
        insertAfter(search(p->data, r, p), p->data); 
        p = p->succ; //转向下一节点
        remove(p->pred);
    } // 仅使用o(1)辅助空间,属于就地算法
} //O(n^2)

6.2 选择排序 selection sort

     选择排序 selection sort也适用于向量与列表之类的序列结构。

     思路:将序列划分为无序的前缀 S[0, r) 及有序的后缀 S[r, n),此后还要求前缀中的元素都不大于后缀中的元素。 如此,每次只需从前缀中选出最大者,并作为最小元素转移至后缀中,即可使有序部分的范围不断扩张。

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

template <typename T> //列表的选择排序算法,对起始于位置 p 的 n 个元素排序
void List<T>::selectionSort ( ListNodePosi(T) p, int n) { //valid(p) && rank(p)+n <= size
    ListNodePosi(T) head = p->pred;
    ListNodePosi(T) tail = p;
    for (int i=0; i<n; i++)
        tail = tail->succ; //将 head 和 tail 指向排序区列表的 header 和 tailer

    while( 1<n ) { //在至少还剩下两个节点之前,在待排序区间内
        ListNodePosi(T) max = selectMax( head->succ, n); //找出最大者
        insertBefore( tail, remove(max) ); // 将无序前缀中的最大者移到有序后缀中作为首元素
        // swap(tail->pred->data, max->data); // 优化:可以不用按上面进行删除和插入操作,只需互换数值即可, 习题 3-13
        tail = tail->pred;
        n--; //n用于记录前缀的宽度
    }
} //O(n^2)

template <typename T> //从起始于位置 p 的 n 个元素中选出最大者,相同的返回最后者
ListNodePosi(T) List<T>::selectMax( ListNodePosi(T) p, int n) {
    ListNodePosi(T) max = p; //最大者暂定为首节点 p
    for ( ListNodePosi(T) cur = p; 1 < n; n--) //从首节点 p 出发,将后续节点逐一与 max 比较
        if ((cur=cur->succ)->data >= max->data) //若当前元素 >= max,max = cur;
    return max; //返回最大节点位置
} //O(nlogn)

插入排序和选择排序的对比:
     有序序列和无序序列次序的颠倒:选择排序中,有序部分是后缀,而无序部分是前缀,且前缀的无序部分都不能超过有序后缀的最小值;而插入排序没有这样的限定。

6.3 归并排序

template <typename T> //有序列表的归并:当前列表中自 p 起的 n 个元素,与列表 L 中自 q 起的 m 个元素归并
void List<T>::merge( ListNodePosi(T) &p, int n, List<T>& L, ListNodePosi(T) q, int m) {
    //assert: this.valid(p) && rank(p)+n<=_size && this.sorted(p,n)
    //        L.valid(q) && rank(q)+m<=L._size && L.sorted(q,m)
    //注:在归并排序之类的场合,有可能 this==L && rank(p)+n=rank(q)
    //为方便归并排序,归并所得的有序列表依然起始于节点 p
    ListNodePosi(T) pp = p->pred; //方便之后能返回 p

    while ( 0 < m ) //在 q 尚未移出区间之前
        if ( (0<n) && (p->data <= q->data) ){ //若 p 仍在区间内且 v(p) <= v(q)
            if ( q == ( p=p->succ ) ) // 如果此时 p 部分已经处理完,则提前返回
                break;
            n--;  // p 归入合并的列表,并替换为其直接后继
        }
        else { //若 p 已超出右界或 v(q) < v(p) 则
            ListNodePosi(T) bb = insertB( p, L.remove( (q=q->succ)->pred )); //将 q 转移到 p 之前
            m--;
        }
    p = pp->succ; //确定归并后区间的起点
}

template <typename T> //列表的归并排序算法:对起始于位置 p 的 n 个元素排序
void List<T>::mergeSort( ListNodePosi(T) & p, int n) { //valid(p) && rank(p)+n <= _size
    if (n<2) 
        return;

    int m = n >> 1; //以中点为界
    ListNodePosi(T) q = p;
    for ( int i=0; i<m; i++) //均分列表
        q = q->succ; 

    mergeSort(p, m);
    mergeSort(q, n-m); //对前后子列表排序

    merge(p, m, *this, q, n-m); //归并
}//注意:排序后,p 依然指向归并后区间的起点 o(nlogn)

欢迎关注【OAOA

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值