手把手教你实现一个链表

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/young2415/article/details/84986379

写在前面

这篇文章是看了清华大学邓俊辉教授的《数据结构(C++语言版)》(第三版)中的「列表」那一章之后写的,列表是链表结构的一般化推广,我在这篇文章中是以双向链表为例写的代码,故类名为LinkedList,但是文章中的代码是基于书中代码进行修改的。

概述

关于链表(LinkedList)的定义,在这里我不做过多的解释了,如果有不知道什么是链表的读者,请参考维基百科,戳这里:维基百科-链表

简单来说,链表是一种链式存储结构的线性表,其中的元素我们称之为节点(node),节点的逻辑顺序与物理顺序并不一致。链表有很多种,比如单向链表、双向链表等,如果把链表头尾相连,就构成了单向循环链表和双向循环链表。今天就以双向链表为例,带大家学习链表这个数据结构。

双向链表图示.png

链表提供哪些接口

在看链表的接口之前,我们首先应该定义链表的基本组成单位——节点。将节点定义为模板类ListNode,然后,用ListNode的实例去创建链表。ListNode模板类定义如下。

//这里并未对ListNode进行封装,可直接对ListNode的数据域和指针域进行读写操作
template <typename T> class ListNode {
public:
	T data; //微信
	ListNode<T> * pred; //前驱
	ListNode<T> * succ; //后继
	ListNode(){} //针对header和trailer的构造
	ListNode(T e, ListNode<T> * p = NULL, ListNode<T> * s = NULL) :data(e), pred(p), succ(s) {} //默认构造器
};

下面是链表提供的接口。

操作接口 功能 适用对象
size() 报告当前列表的规模(节点总数) 列表
first()、last() 返回首、末节点的位置 列表
insertAsFirst(e) 将 e 当作首节点插入 列表
insertAsLast(e) 将 e 当作末节点插入 列表
insertBefore(p, e) 将 e 当作节点 p 的直接前驱插入 列表
insertAfter(p, e) 将 e 当作节点 p 的直接后继插入 列表
remove§ 删除位置p处的节点,返回其数值 列表
disordered() 判断所有节点是否乱序 列表
sort() 调整各节点的位置,使之按非降序排列 列表
find(e) 查找目标元素 e,失败时返回NULL 列表
search(e) 查找目标元素 e,返回不大于 e 且秩最大的节点 有序列表
deduplicate() 剔除重复节点 列表
uniquify() 剔除重复节点 有序列表
get( r ) 获取第 r 个位置上的节点的值 列表
set(r, e) 将第 r 个节点的值改为 e 列表

根据上表定义的接口,可定义LinkedList模板类如下。

#include "ListNode.h" //引入列表节点类

template <typename T> class LinkedList { //列表模板类
private:
	int _size; //规模
	ListNode<T>* header; //头哨兵
	ListNode<T>* trailer; //尾哨兵

protected: /* 内部函数 */
	void init() { //初始化,创建列表对象时统一调用
		header = new ListNode<T>; //创建头哨兵节点
		trailer = new ListNode<T>; //创建尾哨兵节点
		header->succ = trailer; header->pred = NULL; //互联
		trailer->pred = header; trailer->succ = NULL; //互联
		_size = 0; //记录规模
	}

	void copyNodes(ListNode<T>* p, int n) { //O(n)
		init(); //创建头、尾哨兵节点并做初始化
		while (n--) { //将起自p的n项依次作为末节点插入
			insertAsLast(p->data);
			p = p->succ;
		}
	}

    int clear(){ //清除所有节点
        int oldSize = _size;
   	    while ( 0 < _size ) remove ( header->succ ); //反复删除首节点,直至列表变空
        return oldSize;
    }

	/*二路归并
	  不仅修改指针指向的内容,而且要对指针本身进行修改,所以参数是引用类型*/
	void merge(ListNode<T>* &p, int n, LinkedList<T> &L, ListNode<T>* q, int m) {
		ListNode<T>* pp = p->pred; //借助前驱(可能是header),以便返回当前节点
		while (0 < m) { //在q尚未移出区间之前
			if ((0 < n) && p->data < q->data) { //若p仍在区间内,且p->data小于等于q->data
				if (q == (p = p->succ))	break; n--; //则p归入合并的链表,并替换为直接后继
			}else { //若p已超出右界,或q->data小于p->data,
				insertBefore(p, L.remove((q = q->succ)->pred)); //则将q转移至p之前
				m--;
			}
		}
		p = pp->succ; //确定归并后区间的(新)起点
	}

	/*链表的归并排序算法,对起始于位置p的n个元素排序*/
	void mergesort(ListNode<T>* &p, int n) {
		//assert(valid(p) && rank(p) + n <= _size);
		if (n < 2) return; //若待排序范围已足够小,则直接返回
		ListNode<T>* pp = p->pred;

		//递归
		int m = n >> 1; //以中点为界
		ListNode<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 = pp->succ;
	}

public:
// 构造函数
	/*默认构造函数*/
	LinkedList () {
		init();
	}

	//整体复制列表L
    LinkedList ( LinkedList<T> & L ) {  
    	copyNodes ( L.first(), L._size ); 
    }
    
    //复制L中自第r项起的n项(assert: r+n <= L._size)
    LinkedList ( LinkedList<T> const& L, int r, int n ) {
        copyNodes ( L[r], n );
    }
    
    //复制列表中自位置p起的n项(assert: p为合法位置,且至少有n-1个后继节点)
    LinkedList ( ListNode<T> p, int n ) {
        copyNodes ( p, n );
    }

// 析构函数
    ~LinkedList(){ //释放(包含头、尾哨兵在内的)所有节点
        clear(); 
        delete header; 
        delete trailer;
    }

// 只读访问接口
	int size() const { //规模
		return _size;
	}

	bool empty() const { return _size <= 0; } //判空

	//重载操作符 “[]”,支持循秩访问
	T operator[](int r) const { //O(r),效率低下,可偶尔为之,却不宜常用
		ListNode<T>* p = first(); //从首节点出发
		while (0 < r--) p = p->succ; //顺数第r个节点即是
		return p->data; //目标节点
	} //任一节点的秩,亦即前驱的总数

	//此函数功能和重载操作符 “[]” 一样,都是返回第 r 个节点的数值
	T get(int r) const { //O(r)
		ListNode<T>* p = first(); //从首节点出发
		while (0 < r--) p = p->succ; //顺数第r个节点即是
		return p->data; //目标节点
	} //任一节点的秩,亦即前驱的总数

	/*返回首节点*/
	ListNode<T>* first() const {
		return _size == 0 ? NULL : header->succ;
	}

	/*返回末节点*/
	ListNode<T>* last() const {
		return _size == 0 ? NULL : trailer->pred;
	}
	//判断位置p是否对外合法
    bool valid ( ListNode<T> p ) { 
		return p && ( trailer != p ) && ( header != p ); 
	} //将头、尾节点等同于NULL

	/*判断所有节点是否是乱序,如果是,返回true,否则返回false
	  要求:T 为基本类型或已实现了对运算符 “>” 的重载*/
	int disordered() const { //统计逆序相邻元素对的总数
		int n = 0; ListNode<T>* p = first();
		for ( int i = 0; i < _size - 1; p = p->succ, i++ )
			if ( p->data > p->succ->data ) n++;
		return n;
	}

	//无序列表查找
	ListNode<T>* find(T const & e) const {
		return find ( e, _size, trailer );
	}

	/*查找
	  在节点p(可能是trailer)的 n 个(真)前驱中,找到等于 e 的最后者
	  要求:T 为基本类型或已实现了对运算符 “==” 的重载*/
	ListNode<T>* find(T const & e, int n, ListNode<T>* p) const{ //顺序查找,O(n)
		while (0 < n-- && p->pred != NULL) //从右向左,逐个将 p 的前驱与 e 比对
			if (e == (p = p->pred)->data)
				return p; //直至命中或范围越界
		return NULL; //若越出左边界,意味着查找失败
	}//header 的存在使得处理更为简洁

	/*在有序列表内节点p(可能是trailer)的n个(真)前驱中,找到不大于e的最后者
	  要求:T 为基本类型或已重载操作符 “<=”*/
	ListNode<T>* search(T const& e, int n, ListNode<T>* p) const {
		// assert: 0 <= n <= rank(p) < _size
		while (0 <= n--) //对于p的最近的n个前驱,从右向左逐个比较
			/*DSA*/ {
			if (((p = p->pred)->data) <= e) break; //直至命中、数值越界或范围越界
			/*DSA*/
		}
		// assert: 至此位置p必符合输出语义约定——尽管此前最后一次关键码比较可能没有意义(等效于与-inf比较)
		return p; //返回查找终止的位置
	} //失败时,返回区间左边界的前驱(可能是header)——调用者可通过valid()判断成功与否
	
	//有序列表整体查找
	ListNode<T>* search(T const& e) const {
		return search(e, _size, trailer);
	}

// 可写访问接口
	ListNode<T>* insertAsFirst(T const& e) { //将e作为首节点插入链表
		return insertAfter(header, e); //相当于 e 作为头哨兵header的后继插入
	}

	ListNode<T>* insertAsLast(T const& e) { //将e作为末节点插入链表
		return insertBefore(trailer, e); //相当于 e 作为尾哨兵trailer的前驱插入
	}

	/*e 当做 p 的前驱插入*/
	ListNode<T>* insertBefore(ListNode<T>* p, T const& e) {
		ListNode<T>* x = new ListNode<T>(e, p->pred, p); //创建(耗时100倍)
		p->pred->succ = x; //建立链接,返回新节点的位置
		p->pred = x;
		_size++;
		return x;
	}

	/*e 当做 p 的后继插入*/
	ListNode<T>* insertAfter(ListNode<T>* p, T const& e) {
		ListNode<T>* x = new ListNode<T>(e, p, p->succ); //创建(耗时100倍)
		p->succ->pred = x;
		p->succ = x;
		_size++;
		return x;
	}
	/*删除合法位置p处节点,返回其数值*/
	T remove(ListNode<T> * p) {
		T e = p->data; //备份待删除节点数值(假设类型T可直接赋值)
		p->pred->succ = p->succ;
		p->succ->pred = p->pred;
		delete p; //释放内存
		_size--;
		return e; //返回备份数值
	}

	/*链表区间排序*/
	void sort(ListNode<T>* p, int n) {
		mergesort ( p, n );  //归并排序
	}

	//链表整体排序
	void sort() {
		sort(first(), _size);
	}

	int deduplicate() { //剔除无序链表中的重复节点
		if (_size < 2) return 0; //平凡链表自然无重复
		int oldSize = _size; //记录原规模
		ListNode<T>* p = header; int r = 0; //p从首节点开始
		while (trailer != (p = p->succ)) { //依次直到末节点
			ListNode<T>* q = find(p->data, r, p); //在p的r个(真)前驱中查找雷同者
			q ? remove(q) : r++; //若的确存在,则删除之;否则秩加一
		} //assert: 循环过程中的任意时刻,p的所有前驱互不相同
		return oldSize - _size; //列表规模变化量,即被删除元素总数
	}

	/*有序链表的去重操作,成批剔除重复元素,效率更高
	  要求:链表中的元素是有序的,且类型 T 是基本类型或已重载操作符 “!=”*/
	int uniquify() { 
		if (_size < 2) return 0; //平凡链表自然无重复
		int oldSize = _size; //记录原规模
		ListNode<T>* p = first(); ListNode<T>* q; //p为各区段起点,q为其后继
		while (trailer != (q = p->succ)) //反复考查紧邻的节点对(p, q)
			if (p->data != q->data) p = q; //若互异,则转向下一区段
			else remove(q); //否则(雷同),删除后者
		return oldSize - _size; //列表规模变化量,即被删除元素总数
	}
	
	/*若 r 大于等于 0,将第 r 个节点的值改为 e
	  若 r 小于 0,将倒数第 -r 个节点的值改为 e*/
	void set(int r, T e) {
		if (r > 0 && r >= _size) return;
		if (r < 0 && -r > _size) return;

		if (r >= 0) {
			ListNode<T>* p = first(); //从首节点出发
			while (0 < r--) p = p->succ; //顺数第r个节点即是
			p->data = e;
		}
		else {
			ListNode<T>* p = last(); //从首节点出发
			while (0 > ++r) p = p->pred; //顺数第r个节点即是
			p->data = e;
		}
	}
};

LinkedList模板类的定义中,sort()接口是用归并排序算法实现的,关于归并排序,我在这篇文章中提到过:归并排序,感兴趣的读者可以读一下。

参考文献:数据结构(C++语言版),邓俊辉 编著,清华大学出版社,ISBN 978-7-302-33064-6

欢迎关注我的微信公众号,扫描下方二维码或微信搜索:AProgrammer,就可以找到我,我会持续为你分享 IT 技术。

猜你喜欢

转载自blog.csdn.net/young2415/article/details/84986379