手把手教你实现LinkedList

概述

关于链表(LinkedList)的定义,这里我就不作解释了,如果你还不知道什么是链表,那么本文并不适合你阅读。同时本文使用C++实现,如果你还不懂C++的基础语法,那么本文也不适合你阅读。今天我以单链表为例教你实现一个链表。小编是一名大二学生,学习C++也不过几个月的时间,如果有写错的地方,还望斧正!


接口设计

接口 功能
add 添加
insert 插入
remove 删除
clear 清空
set 修改
get 按索引获取
indexOf 搜索
forEach 遍历

在C++中接口是通过抽象类是实现的,因此我们定义一个类,名为AbstractList,同时将方法添加进去

template<typename T>
class AbstractList {
     
public:
    virtual ~AbstractList() {
     }
public:
    // 增
    virtual AbstractList<T>& add(T elem) = 0;
    virtual AbstractList<T>& insert(T newElem, size_t pos) = 0;

    // 删
    virtual AbstractList<T>& remove(size_t pos) = 0;
    virtual AbstractList<T>& clear() = 0;

    // 改
    virtual AbstractList<T>& set(size_t index, T newElem) = 0;

    // 查
    virtual const T& get(size_t index) const = 0;
    virtual size_t indexOf(T target) const = 0;

    // 遍历
    virtual AbstractList<T>& forEach(const std::function<void(T)>& fn) = 0;
};

实现LinkedListNode和LinkedList

1.定义LinkedListNode

首先定义节点类,名为LinkedListNode,该类可以通过struct定义在LinkedList的内部,也可以像我一样使用class单独定义为一个类,为了方便我将LinkedList声明为LinkedListNode的友元类,这样LinkedList就能访问到该类的所有属性了,不过在声明友元之前,LinkedList尚未编译,所以要先声明LinkedList。同时为了检测内存泄漏,我在LinkedListNode中定义了一个静态属性 CNT,这个属性在构造函数中+1,在析构函数中-1。如果确认内存不泄漏,可以将该属性删除。

// 前向声明
template<typename T>
class LinkedList;

template<typename T>
class LinkedListNode {
     
    friend class LinkedList<T>;
public:
    // CNT 用来检查内存泄漏
    static int CNT;
private:
    T _value;// 数据域
    LinkedListNode<T>* next;  //后继
public:
    LinkedListNode(T value, LinkedListNode<T>* next) : _value(value), next(next) {
     
        CNT++;
    }

    virtual ~LinkedListNode() {
     
        CNT--;
    }
};
template<typename T>
int LinkedListNode<T>::CNT = 0;

2.定义LinkedList

首先 公有继承AbstractList类 ,将接口导入进来,该类的属性有_head,_tail,_length,均为私有属性,_head是头结点指针,_tail是尾结点指针,_length是链表的长度。该类需要实现拷贝构造函数、需要重载operator=,将默认的拷贝构造函数和默认的赋值运算符都改为深拷贝,并且需要实现析构函数来释放new出来的内存。

template<typename  T>
class LinkedList: public AbstractList<T>{
     
private:
    LinkedListNode<T>* _head = nullptr;
    LinkedListNode<T>* _tail = nullptr;
    size_t _length = 0;
public:
    LinkedList<T>& add(T elem) override {
     }

    LinkedList<T>& insert(T newElem, size_t pos) override {
     }

    LinkedList<T>& remove(size_t pos) override {
     }

    LinkedList<T>& clear() override {
     }

    LinkedList<T>& set(size_t index, T newElem) override {
     }

    const T& get(size_t index) const override {
     }

    size_t indexOf(T target) const override {
     }

    LinkedList<T>& forEach(std::function<void(T)> fn) override{
     }
};

实现接口

1.实现add
    /**
     * 尾插
     * @param data 新的数据
     * @return 自身
     */
    LinkedList<T>& add(T data) override {
     
        // 生成结点
        LinkedListNode<T>* pNode = new LinkedListNode<T>(data, nullptr);
        // 如果是空链表
        if (!_length) {
     
        	// 如果头指针不为空,则先把头指针内存释放
            if (_head != nullptr)delete _head;
            // 头指针指向新的节点
            _head = new LinkedListNode<T>(data, pNode);
            // 尾指针指向新的节点
            _tail = pNode;
        } 
        // 如果不是空链表
        else {
     
	        // 尾指针后移
            _tail->next = pNode;
            // 尾指针指向新生成的结点
            _tail = pNode;
        }
        // 长度+1
        _length++;
        // 返回自身,便于函数式编程
        return *this;
    }
2.实现remove
	/**
	 * 删除
	 * @param index要删除元素的索引 
	 * @return 自身
	 */
    LinkedList<T>& remove(size_t index) override {
     
    	// 如果要删除元素的索引错误,则抛出异常
        if (index > _length - 1) {
     
            throw exception("remove:溢出!");
        }
        // 开始遍历链表,注意:这里要从从头指针开始遍历,因为要删除的元素的索引可能为0
        int i = -1;
        for (LinkedListNode<T>* ptr = _head; ptr; ptr = ptr->next,i++) {
     
	        // 找到目标元素的前驱结点
            if (i == index - 1) {
     
            	// 先存一下目标结点
                auto temp = ptr->next;
                // 将前驱结点的后继改为目标结点的后继
                ptr->next = ptr->next->next;
                // 删除目标结点
                delete temp;
                // 如果删除的是尾结点
                if (index == _length - 1) {
     
	                // 尾结点前移
                    _tail = ptr;
                }
                // 退出循环
                break;
            }
        }
        // 长度-1
        _length--;
        // 如果删除之后变为空链表
        if (_length == 0) {
     
        	// 释放内存
        	delete _head;
	        //将头指针和尾指针置空
            _head = _tail = nullptr;
        }
        return *this;
    }
3.实现clear
    LinkedList<T>& clear() override {
     
	    // 如果是空链表,直接返回自身
        if (!_length)return *this;
        // 工作指针从头结点开始遍历链表
        LinkedListNode<T>* ptr = _head;
        while (ptr) {
     
	        // 临时存一下当前指向的节点
            auto temp = ptr;
            // 工作指针后移
            ptr = ptr->next;
            // 释放内存
            delete temp;
        }
        // 头指针,尾指针置空
        _head = _tail = nullptr;
		// 长度置0
        _length = 0;
        // 返回自身
        return *this;
    }
4.实现insert
   /**
     * 插入
     * @param newElem 新的元素
     * @param pos  要插入的位置
     * @return 自身
     */
    LinkedList<T>& insert(T newElem, size_t pos) override {
     
	    // 如果要插入的位置大于等于链表的长度,则直接尾插
        if (pos >= _length) return add(newElem);
        // 生成一个结点,后继待定
        LinkedListNode<T>* pNode = new LinkedListNode<T>(newElem, nullptr);
        // 从头指针开始遍历链表
        int i = -1;
        for (LinkedListNode<T>* ptr = _head; ptr; ptr = ptr->next,i++) {
     
	        // 找到目标结点的前一个结点
            if (i == pos - 1) {
     
	            // 新结点的后继指向目标结点
                pNode->next = ptr->next;
                // 前一个结点的后继指向新节点
                ptr->next = pNode;
                // 退出循环
                break;
            }
        }
        // 长度+1
        _length++;
        // 返回自身
        return *this;
    }
5.实现indexOf
    /**
     * 按值查找
     * @param target 要查找的元素
     * @return 该元素对应的索引
     */
    size_t indexOf(T target) const override {
     
        int i = 0;
        // 遍历链表
        for (LinkedListNode<T>* ptr = _head->next; ptr; ptr = ptr->next,i++) {
     
        	// 找到该元素,返回该元素
            if (ptr->_value == target) {
     
                return i;
            }
        }
        // 未找到抛出异常
        throw exception("find:未找到该元素!");
    }
6.实现set
    /**
     * 修改
     * @param index 要修改元素的索引 
     * @param newElem 新的值
     * @return 自身
     */
    LinkedList<T>& set(size_t index, T newElem) override {
     
	    // 如果索引不正确,抛出异常
        if (index > _length - 1) {
     
            throw exception("set:溢出!");
        }
        // 遍历链表
        int i = 0;
        for (LinkedListNode<T>* ptr = _head->next; ptr; ptr = ptr->next) {
     
	        // 找到要修改的元素
            if (i++ == index) {
     
            	// 修改值
                ptr->_value = newElem;
                // 返回自身
                return *this;
            }
        }
    }
7.实现get
   /**
     * 搜索
     * 通过该方法返回的值只读
     * @param index 索引 
     * @return 该索引对应的数据 
     */
    const T& get(size_t index) const override {
     
        if (_length == 0)throw exception("get:空链表!");
        else if (index >= _length) throw exception("get:溢出!");
        // 遍历链表,i用于计数
        int i = 0;
        for (LinkedListNode<T>* ptr = _head->next; ptr; ptr = ptr->next) {
     
        	// 找到对应的结点,就返回
            if (i++ == index) {
     
                return ptr->_value;
            }
        }
    }
8.实现forEach

我们会发现get方法获取一个值就需要遍历一次链表,时间复杂度为O(n)。
此时我们想一下这种情况:
在外界如何打印该链表?
你可能会说利用int i 游标使用get方法遍历链表,源代码如下:

    LinkedList<int> list;
    list.add(1);
    list.add(2);
    list.add(3);
    for (int i = 0; i < list.length(); ++i) {
     
        cout << list.get(i) << "\t";
    }
    cout << endl;

是的,这确实没错,但是利用get效率实在太低了,这样的算法时间复杂度为O(n²),这怎么能忍?
于是我实现了forEach方法,这个方法可以从外接收一个lambda表达式或函数,这样就可以一边遍历一边调用传进来的函数,代码如下:

    /**
     * 遍历
     * 1.该方法从外界接收一个 返回值为void 参数类型为T的lambda表达式或函数的常引用
     * 传入的lambda表达式或函数的参数的传递方式采用值传递,这是为了避免在forEach遍历
     * 过程中修改到表中的数据,同时也避免了两个(指针)引用指向同一对象
     * 2.传常引用是为了提升效率
     * @param fn lambda表达式或者函数的常引用
     * @return 自身
     */
    LinkedList<T>& forEach(const std::function<void(T)>& fn) override {
     
        if (_length == 0)throw exception("forEach:空链表!");
        for (LinkedListNode<T>* ptr = _head->next; ptr; ptr = ptr->next) {
     
            fn(ptr->_value);
        }
        return *this;
    }

lambda表达式是C++11的新特性 基本语法为:

[捕获列表](参数列表)->返回类型{函数体}
返回类型可以省略

std::function类是C++11新增的特性,在C++11之前你可以通过函数指针实现相同功能,但捕获列表不为空的lambda是不能通过函数指针实现的!
OK,实现forEach之后,遍历效率就高了许多了,你可以使用以下语句进行遍历

    LinkedList<int> list;
    list.add(1);
    list.add(2);
    list.add(3);
    list.forEach([](int item) {
      cout << item << '\t'; });
    cout << endl;

这样看起来就舒服多了!
同时你还可以使用lambda捕获一下变量:

    LinkedList<int> list, list1;
    list.add(1);
    list.add(2);
    list.add(3);
    // 将list中的全部数据加入list1
    list.forEach([&](int item) {
      list1.add(item); });
    // 打印list1
    list1.forEach([](int item) {
      cout << item << "\t"; });// 输出 1,2,3
    cout << endl;

在上面的代码中我将list中的代码加入到了list1中,由于lambda的参数采用值传递,你可以不用担心两个引用指向同一个对象这种问题。

9.添加length方法
   size_t length() const {
     
        return _length;
    }
10.拷贝构造函数
    LinkedList(const LinkedList<T>& src) {
     
	    // 将src中的数据添加到新的链表
        for (LinkedListNode<T>* ptr = src._head->next; ptr; ptr = ptr->next) {
     
            add(ptr->_value);
        }
    }
11.operator=
    LinkedList& operator=(const LinkedList<T>& src) {
     
	    // 判断自身复制
        if (&src == this) {
     
            return *this;
        }
        // 先清空
        clear();
        // 从src添加新的数据
        for (LinkedListNode<T>* ptr = src._head->next; ptr; ptr = ptr->next) {
     
            add(ptr->_value);
        }
        //返回自身的引用
        return *this;
    }
12.析构函数

直接清空即可

 ~LinkedList() override {
      clear(); }

OK,至此,这个单链表已经基本实现了,你还可以添加一些其他功能,例如:sort,reverse等等,你也可以选择重载一下其他运算符,你也可以想想跳跃表和普通链表有哪些优势,你还可以把这个类扩展为Stack或者Queue,使这个类更方便、快捷、好用,不要局限于书上的那些简单算法和数据结构,把思维打开才能获得更多知识!

源代码

我已经将代码提交到github上,大家可以下载:
https://github.com/Ozral/LinkedList

你可能感兴趣的:(数据结构,c++,算法,链表,数据结构,java)