list的实现

文章目录

    • 一.什么是list
    • 二.迭代器
      • 1.迭代器的功能分类
      • 2.list的普通迭代器
      • 3.list的const迭代器
      • 4.list迭代器的失效问题
      • 5.list迭代器最终版
      • 6.迭代器的价值
    • 三.一些注意点
    • 四.list与vector对比
      • vector:
      • list
    • 五.list整体实现代码

一.什么是list

list是STL中的一个容器,底层结构是一个带头双向循环链表。即每一个节点都有两个指针分别指向这个节点的前驱和后继节点,且这个容器一旦被创建出来即使不插入任何数据也应该有一个哨兵位(并且指针都是指向本身)。

list的每一个节点都是这样的结构:

template
struct __list_node
{
 	__list_node*_prev;
    __list_node*_next;
    T _data;
};

二.迭代器

1.迭代器的功能分类

单向迭代器:只能++,不能–。如:单链表,哈希表

双向迭代器:既能++又能–,如双向链表

随机访问迭代器:不但能++和–,还能+和-,比如string和vector

迭代器是内嵌类型(内部类或者定义在类里面)

2.list的普通迭代器

string和vector的迭代器我都是采用原生指针来实现的,这是因为它们的底层结构本身就是一个数组,空间是连续的,所以原生指针正好就能满足我们的需求(解引用就能拿到指向的数据,++就能拿到下一个元素)。

但是对于list来说,仅仅是原生指针就显得无能为力,链表的节点之间的空间并不连续,甚至后面开辟的节点的地址比前面开辟的节点的地址还要小。所以原生指针的++并不能得到这个节点的下一个节点,并且对节点指针解引用也无法拿到节点的数据(对节点解引用只能拿到节点)。


通过node->data就可以拿到node对应的数据,通过node->next就可以拿到node的下一个节点。但直接解引用和++做不到这一点,所以我们可以采用运算符重载改变运算符的规则来达到目的(此刻你就是规则的制定者)。

对原生指针封装并改变其对应的操作符规则就可以达到我的目的

template
struct __list_iterator
{
    typedef __list_node node;
    node*pnode;
    
    __list_iterator(node*p)//用一个节点的指针初始化
        :pnode(p)
        {}
    
  	T& operator*()
    {
        return pnode->_data;//解引用拿到节点的数
    }
    
    __list_iterator& operator++()
    {
        pnode=pnode->_next;
        return *this;//返回的还是迭代器
    }
    
    bool operator!=(const __list_iterator& it)
    {
        return pnode!=it.pnode;
    }
};

struct和class都是创建类,但是struct默认是公有,class默认是私有

3.list的const迭代器

使用原生指针实现const迭代器的时候,只要在最开始加一个const即可,但是对于这个迭代器来说是不能通过在最左边加const来防止迭代器指向的数据不被修改的。

那么这里可以针对const迭代器重新封装一个类:

template
struct __list_const_iterator
{
    typedef __list_node node;
    node* pnode;
    
    __list_iterator(node*p)
       :pnode(p)
       {}
    
    const T&operator*()const
    {
        return pnode->_data;
    }
    
 	__list_const_iterator&operator++()
    {
        pnode=pnode->_next;
        return *this;
    }
    
    bool operator!=(const __list_const_iterator&it)
    {
        return pnode!=it.pnode;
    }
}

const迭代器与普通迭代器最大的不同就是const迭代器解引用拿到的数据不可修改,但是仍让要支持++


观察发现将普通迭代器和const迭代器各自封装成一个类实在是有些冗余了,因为它们之间除了对解引用的操作有所不同之外其他的都是一样的。

因此可以采用泛型编程的思想将解引用函数的返回值设置成一个模板参数,这样只要在使用时用户传不同的模板参数编译器就会生成不同的类,库中也是采用的这种实现方式。

template
struct __list_iteraotr
{
  typedef __list_node node;
  typedef __list_iterator Self;
   node*pnode;
    
   __list_iterator(node*p)
       :pnode(p)
       {}
    
    Ref oprator*()
    {
        return pnode->_data;
    }
    Self &operator++()
    {
        pnode=pnode->_next;
        return *this;
    }
    
    bool operator!=(const Self&it)
    {
        return pnode!=it.pnode;
    }
    
};

4.list迭代器的失效问题

erase失效,删除这个节点以后这个节点的空间就会被删除,所以指向这个被删除节点的迭代器就会失效。

insert不失效,插入是在这个节点前插入并不影响这个节点。

5.list迭代器最终版

普通迭代器和const迭代器都有了,但是迭代器还没有被实现完毕。迭代器的行为是像指针一样,那么对于一个结构体来说,当我有这个结构体的指针,如果要访问结构体中的元素,就需要使用->这个运算符,所以这里还要重载一下这个符号。但是const迭代器不能被更改所以对于->这个运算符也要针对const和非const类型都各重载一个,所以这里也要用模板参数,至此迭代器已经有了三个模板参数

template
struct __list_iterator
{
  typedef __list_node node;
  typedef __list_iterator Self;
  
  Ref& operator*()
  {
      return pnode->_data;
  }
    
 //这里有两层封装,迭代器是节点的指针解引用是拿到节点,节点再解引用才是数据,但是这里是希望一次解引用->直接拿到数据
 //所以这里返回数据的地址
  Ptr operator->()
  {
      return &(pnode->data);
  }
    
  Self&operator++()
  {
      pnode=pnode->_next;
      return *this;
  }
  
  Self operator++(T)//后置++
  {
      Self tmp(*this);
      pnode=pnode->_next;
	  return tmp;      
  }
  
  Self&operator--()
  {
	pnode=pnode->_prev;
    return *this;
  }
      
  Self operator--(T)
  {
      Self tmp(*this);
      pnode=pnode->_prev;
      return tmp;
  }
  
  bool operator!=(const Self&it)
  {
      return pnode!=it.pnode;
  }
    
  bool operator==(const Self&it)
  {
      return pnode==it.pndoe;
  }
};

6.迭代器的价值

迭代器封装了底层的实现细节,但是它为我们访问容器提供了统一的方式降低了我们的学习成本。各种容器之间的实现方式都是不同的,结构也不同,即不同容器之间的访问方式都是不一样的。但是迭代器的实现就方便了我们,尽管后面用的set是一个搜索二叉树我们仍然可以使用迭代器像现在这样访问。

三.一些注意点

1.标准库中的list提供了排序的函数,list的迭代器不支持随机访问所以list无法进行三数取中,也就是说list用不了快排。库中提供的sotr函数,其实一个归并排序,效率很低甚至低于将数据拷贝到vector排序完再拷贝回来。所以要少用,要排序尽量还是用vector。

2.对于一般的类来说,类名就是类型,但是对于类模板来说,类型=类名+模板参数,如list的类型是list< int >

四.list与vector对比

vector:

vecotr的优点(结构优势):

1.支持下标随机访问

2.尾插尾删效率高(扩容的那一次会比较慢)

3.CPU高速缓存命中率高(从缓存中加载数据是一次加载一段数据,vector中的数据地址是连续的,也就是说加载的一段数据可能都是vector数组中的)


vector的缺点:

1.非尾插尾删效率低

2.扩容有代价并且有一定的空间浪费


vector迭代器失效问题:

insert(野指针)和erase(意义不同)均失效

其实string中insert也存在迭代器失效的问题,但是string中的接口几乎都是使用下标来访问的,所以在实现string时没有考虑迭代器失效的问题

list

list的优点:

1.空间按需申请释放,没有空间浪费

2.任意位置插入删除为O(1)—不考虑查找的时间


list的缺点:

1.不支持随机访问,查找效率低

2.CPU高速缓存命中率低

3.每个节点中除了存数据外还要额外存两个指针


list迭代器失效问题:

erase失效,insert不失效

五.list整体实现代码

#pragma once
#include
#include
#include


//list本质是一个带头双向循环链表,因为链表空间的不连续性,所以list的迭代器不能再使用原生指针,
//通过封装+运算符重载改变符号的访问规则,来提供迭代器像指针一样的行为

namespace wbm
{
	
	template
	struct __list_node
	{
		__list_node* next;
		__list_node* prev;
		T data;

		//初始化链表的节点
		__list_node(const T&x)
			:next(nullptr)
			,prev(nullptr)
			,data(x)
		{}
	};


	//要有const和非const的解引用,一个可供修改,一个不能修改,为了避免代码冗余,可以使用双参数模板
	template
	struct __list_iterator //目的是通过封装和运算符重载达到类似指针,指向节点的指针
	{
		typedef __list_iteratorSelf;
		typedef __list_node node;
		node* pnode;

		//构造初始化
		__list_iterator(node *p)
			:pnode(p)
		{}

		//返回值取决于内部用的成员,解引用拿到的是数据,++则是比较指针

		Ref operator*()
		{
			return pnode->data;
		}

		Self& operator++()
		{
			pnode = pnode->next;
			return *this;
		}

		Self operator++(T)//后置++,先返回再++
		{
			//node* tmp = pnode;
			Self tmp(*this);//创建一个迭代器对象,然后用*this构造
			pnode = pnode->next;

			return tmp;
		}
		Self& operator--()
		{
			pnode = pnode->prev;
			return *this;
		}
		Self operator--(T)
		{
			//node* tmp = pnode;
			Self tmp(*this);
			pnode = pnode->prev;
			return tmp;
		}

		//重载!=,是两个迭代器之间的比较
		bool operator!=(const Self& it)
		{
			return pnode != it.pnode;
		}

		bool operator==(const Self& it)
		{
			return pnode == it.pnode;
		}

		//重载箭头,
		Ptr operator->()
		{
			//方便直接访问
			return &(pnode->data);//迭代器是节点的指针,要拿到节点的数据要对节点解引用,pnode就是节点
		}
	};

	template
	class list
	{
		typedef __list_node node;
		

	public:
		typedef __list_iterator iterator;
		typedef __list_iterator const_iterator;

		void emptyInit()
		{
			_head = new node(T());//用一个匿名对象来初始化哨兵位,类成员函数有个默认的this指针,new的用法为new+类型
			_head->next = _head;
			_head->prev = _head;
			_size = 0;
		}

		//上手一个构造,将哨兵位衔接起来
		list()
		{
			emptyInit();//复用代码可以提高代码的可维护性
		}

		//迭代器区间构造的构造函数
		template
		list(InputIterator first, InputIterator last)
		{
			emptyInit();
			while (first != last)
			{
				push_back(*first);//对迭代器解引用就可以拿到迭代器内部的数据
				++first;
			}
		}

		//拷贝构造和赋值重载
		//拷贝构造本质就是一个构造函数的重载:lt(lt1),类对象之间的初始化
		list(const list<)
		{
			//将前面那个节点全部拷贝一份尾插到我的头节点中
			emptyInit();

			for (auto& e : lt)//这里要用引用避免拷贝带来的巨大代价
			{
				push_back(e);//push_back调用insert,内部会用e来初始化节点
			}
		}
		
		list& operator=(list lt)//这里传值时会发生拷贝,直接占用这个空间即可
		{
			swap(lt);
			return *this;
		}

		void swap(list<)
		{
			std::swap(_head,lt._head);//交换两者之间的头节点
			std::swap(_size, lt._size);
		}
		//搞迭代器方便访问和打印
		iterator begin()
		{
			//哨兵位的下一个
			return iterator(_head->next);
		}

		iterator end()
		{
			//尾节点的下一个,就是哨兵位
			return iterator(_head);
		}

		const_iterator begin()const
		{
			return const_itertator(_head->next);
		}

		const_iterator end()const
		{
			return const_itertator(_head);
		}

		//尾插函数
		void push_back(const T& val)
		{
			//用一个尾节点标定尾巴,然后将新节点
			/*node* newnode = new node(val);
			node* tail = _head->prev;
			tail->next = newnode;
			newnode->prev = tail;
			newnode->next = _head;
			_head->prev = newnode;*/

			insert(end(), val);
		}

		//尾删,头插头删,插入,删除
		void pop_back()
		{
			//node* tail = _head->prev;
			//_head->prev = tail->prev;
			//tail->prev->next = _head;

			释放tail节点
			//delete tail;

			erase(--end());
		}

		void push_front(const T&x)
		{
			//开辟一个新节点
			/*node* newnode = new node(x);

			newnode->next = _head->next;
			_head->next->prev = newnode;
			newnode->prev = _head;
			_head->next = newnode;*/

			insert(begin(), x);

		}

		void pop_front()
		{
			/*node* next = _head->next;
			_head->next = next->next;
			next->next->prev = _head;
			delete next;*/
			
			erase(begin());
		}

		//在pos位置之前插入
		iterator insert(iterator pos,const T&val)
		{
			//搞一个变量记录pos节点的指针,因为没有重载->
			node* cur = pos.pnode;//cur就相当于这个pos节点的指针
			
			node* newnode = new node(val);
			newnode->prev = cur->prev;
			cur->prev->next = newnode;
			newnode->next = cur;
			cur->prev = newnode;

			_size++;
			//插入以后,迭代器并不失效,因为它指向的还是原位置
			return iterator(newnode);
		}

		iterator erase(iterator pos)//erase以后迭代器失效,因为删除以后这个节点会被释放
		{
			//不能是哨兵位
			assert(pos != end());

			//返回删除节点的后一个节点
			node* cur = pos.pnode;
			node* next = cur->next;
			node* prev = cur->prev;

			next->prev = prev;
			prev->next = next;

			delete pos.pnode;

			_size--;
			return iterator(next);
		}

		void clear()
		{
			//清空掉所有节点,但是要保留哨兵位
			iterator it = begin();
			while (it != end())
			{
				it=erase(it);
				it++;
			}
		}

		bool empty()const
		{
			return _size == 0;
		}

		size_t size()
		{
			return _size;
		}

		//析构函数,调用clear此外再将头节点释放掉
		~list()
		{
			clear();

			delete _head;
			_head = nullptr;
		}

	private:
		node* _head;		//哨兵位,链表节点的指针
		size_t _size;
	};



	void test_list1()
	{
		list lt1;
		lt1.push_back(1);
		lt1.push_back(2);
		lt1.push_back(3);
		lt1.push_back(5);
		lt1.push_back(7);
		lt1.push_back(9);
		

		list::iterator it = lt1.begin();
		while (it != lt1.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
		

		lt1.pop_back();
		lt1.pop_back();
		lt1.pop_back();
		lt1.pop_back();
		
		for (auto e : lt1)
		{
			cout << e << " ";
		}
		cout << endl;

		lt1.push_front(10);
		lt1.push_front(20);
		lt1.push_front(30);
		lt1.push_front(40);

		for (auto e : lt1)
		{
			cout << e << " ";
		}
		cout << endl;

		lt1.pop_front();
		lt1.pop_front();
		lt1.pop_front();
		lt1.pop_front();
		lt1.pop_front();
		for (auto e : lt1)
		{
			cout << e << " ";
		}
		cout << endl;

		it = lt1.begin();//更新之前用过的迭代器

		lt1.insert(it, 20);
		lt1.insert(it, 30);
		for (auto e : lt1)
		{
			cout << e << " ";
		}
		cout << endl;

		lt1.clear();


	}
}

你可能感兴趣的:(C++,list,链表,数据结构,c++)