【Play-with-Data-Structures-master】---part3:链表(LinkedList)(LinkedListStack)(LinkedListQueue)

本文详细介绍了链表的基本概念和实现方法,包括节点的添加、删除等操作,并探讨了链表在栈和队列数据结构中的应用。分析了各种操作的时间复杂度。

参考:https://blog.youkuaiyun.com/LKJgdut/article/details/105283861

1、链表

链表的添加结点的方法如下:

#include <iostream>
#include <cassert>
using namespace std;
template<typename T>
class Node {
public:
	T e;
	Node<T>* next;
	//Node(T e, Node *next) : e(e), next(next) {}

	//Node(T e) : e(e), next(nullptr) {}

	//Node() : e(), next(nullptr) {}
	Node(T e,Node* next){
		this->e = e;
		this->next = next;
	}
	Node(T e) {
		this->e = e;
		this->next = nullptr;
	}
	Node() {
		this->e = NULL;
		this->next = nullptr;
	}
};

template<typename T>
class LinkedList {
private:
	int size;
	Node<T>* dummyHead;
public:
	LinkedList(){
//初始化链表,创建一个结点(称之为头结点),使得指针 dummyHead 指向这个头结点(头结点不包含在链表中)。此时可以用dummyHead 指代头结点。
        //头结点不存储数据,初始化的时候不需要赋予数据。头结点此时也不执行任何结点,头结点的指针为null。
		dummyHead = new Node<T>();
		size = 0;
	}
	int getSize() {
		return size;
	}
	bool isEmpty() {
		return size == 0;
	}
	void add(int index,T e) {
		assert(index >= 0 && index <= size);
//创建一个新的指针,这个指针指向头结点
		Node<T>* prev = dummyHead;
		for (int i = 0; i < index; i++) {
			prev = prev->next;
		}
/* 进入循环的时候,pre指向头结点。
                指向完 i=0 的循环,pre指向链表 下标=0 的位置。
                那么执行完 i=index-1 的循环,pre指向链表中 下标=index-1的位置,刚刚好就是index的前一个位置。
             */

//            Node node = new Node(e);
//            node.next = pre.next;//pre.next 指向 下标 = index 位置,此时node.next 也指向 下标 = index 位置。
//            pre.next = node;//index-1 位置的结点 pre 的指针 next 指向node结点,那么此时node结点就处于index位置

            //以上3句可以使用下面一句替代
		prev->next = new Node<T>(e, prev->next);
		size++;
	}
	void addFirst(T e) {
		add(0, e);
		size++;
	}
	void addLast(T e) {
		add(size, e);
	}
/**
     * 注意区分上面的Node pre = dummyHead 与下面的 Node cur = dummyHead.next
     * 由于上面我们是要添加结点,必须找到结点的前一个元素,而对于index=0的结点,它的前一个元素是头结点dummyHead,
     * 因此我们要从 dummyHead 位置,既链表第一个结点的前一个位置开始遍历,否则会错过链表的第一个结点。
     *
     * 而下面的get、set、contain 操作都是要找到 index 位置的结点,不需要找到 index位置结点的前一个结点。
     * 因此,我们可以直接从链表的第一个结点 dummyHead.next 开始查找。
     */

	T get(int index) {
		assert(index >= 0 && index <= size);
 //头结点的指针:dummyHead.next 指向链表第一个结点,我们新创建一个指针 cur,同样指向链表的第一个结点。
		Node<T>* cur = dummyHead->next;
/*
            进入循环,cur指向链表 下标=0 的位置的结点。
            经历 i=0 的循环后,cur指向链表 下标=1 位置的结点。
            经历 i=index-1 的循环后,cur执行向链表 下标=index 位置的结点。
             */

		for (int i = 0; i < index; i++) {
			cur = cur->next;
		}
		return cur->e;
	}
	T getFirst() {
		return get(0);
	}
	T getLast() {
		return get(size - 1);
	}
	void set(int index,T e) {
		assert(index >= 0 && index <= size);
		Node<T>* cur = dummyHead->next;
		for (int i = 0; i < index; i++) {
			cur = cur->next;
		}
		cur->e = e;
	}
	bool find(T e) {
		Node<T>* cur = dummyHead->next;
		while (cur != NULL) {
			if (cur->e == e) {
				return true;
			}
			else {
				cur = cur->next;
			}
		}
		return false;
	}
	T remove(int index) {
		assert(index >= 0 && index <= size);
		if (!(index >= 0 && index <= size)) {
			cout << "index error" << endl;
		}
		else {
			Node<T>* prev = dummyHead;
			for (int i = 0; i < index; i++) {
				prev = prev->next;
			}
			Node<T>* retNode = prev->next;
			prev->next = retNode->next;
			retNode->next = nullptr;
			size--;
			return retNode->e;
		}
		
	}
	T removeFirst() {
		return remove(0);
	}

	T removeLast() {
		return remove(size - 1);
	}
/**
         * 想要删除数据是元素 e 的结点,我们必须先找到这个结点,然后按上面的删除方法对其进行删除。
         * 上面查找链表是否包含元素 e 的方法contains 是从链表第一个元素开始查找。但是由于我们这里不仅仅需要查找到这个元素,
         * 而且还要删除这个元素,我们就必须找到这个元素的前一个元素。那么我们从虚拟头结点 dummyHead开始寻找,
         * 使用 node.next 判断结点 node.next是否包含元素e,包含就通过 node结点删除 node.next结点。(node结点是要删除结点 node.next 的前一个结点)
         */
	void removeElement(T e) {
		if (!find(e)) {
			cout << "error,don't find element" << endl;
		}
		else {
			Node<T>* prev = dummyHead;
			while (prev->next != nullptr) {
				if (prev->next->e == e) {
					break;
				}
				else {
					prev = prev->next;
				}
			}
//跳出循环有可能因为遍历到最后一个结点,既pre.next=null,也有可能是因为找到相应节点:pre.next.e.equals(e)
        //因此我们这里还需进行判断,当 pre.next != null,说明找到相应节点,再进行删除操作
			if (prev->next != nullptr) {
				Node<T>* delNode = prev->next;
				prev->next = delNode->next;
				delNode->next = nullptr;
				size--;
			}
		}	
	}

	void print() {
		Node<T>* cur = dummyHead->next;
		while (cur != NULL) {
			cout << cur->e << "->";
			cur = cur->next;
		}
		cout << "NULL" <<"   size:"<<getSize()<< endl;
	}
};

事件复杂度分析
//添加——总体来说,添加事件复杂度为O(n)
addLast(e):O(n)、addFirst(e):O(1);add(index,e):O(n/2)=O(n)

//删除——总体来说,删除事件复杂度为O(n)
removeLast(e):O(n);removeFirst(e):O(1);remove(index,e):O(n/2)=O(n)

//修改操作
set(index,e):O(n)

//查找操作——总体来说,查找事件复杂度为O(n)
get(index):O(n);contains(e):O(n);

之前数组有一个查找元素 e的下标的函数:find(e):O(n),对于链表来说,这种方法没用,因为链表即使拿到索引,我们也得从头开始遍历,无法快速访问。因此链表中没有实现这个方法。

2、用链表实现栈

我们只对链表头进行操作,时间复杂度都是O(1),这个特性与栈相同。那么我们就可以使用链表作为底层来实现栈。

#pragma once
#include"LinkedList.h"
#include"Stack.h"

template<typename T>
class LinkedListStack : public Stack<T>{
private:
	LinkedList<T>* linkedlist;
public:
	LinkedListStack() {//链表栈的底层实现是链表,没有容积这个概念
		linkedlist = new LinkedList<T>();
	}
	int getSize() {
		return linkedlist->getSize();
	}
	bool isEmpty() {
		return linkedlist->isEmpty();
	}
//----------------------------------------------------出栈,入栈、获取栈顶元素——链表头既栈顶
	void push(T e) {
		return linkedlist->addFirst(e);
	}
	T pop() {
		return linkedlist->removeFirst();
	}
	T peek() {
		return linkedlist->getFirst();
	}
	void print() {
		linkedlist->print();
	}
};

3、用链表实现队列

我们使用链表来实现队列,由于链表在尾部进行增删的时候,事件复杂度是O(n),而使用链表实现队列,增删中必然有一步是对链表的尾部进行操作,这使得链表实现的队列时间复杂度过高。之前在使用数组实现队列的时候,也会遇到这种增删操作必然有一步时间复杂度是O(n)的情况,既在数组头部进行增删的时候,时间复杂度是O(n)。
  因此,我们之前为了解决这个问题,使用循环队列的方式解决。循环队列也是使用数组实现,但是它没有直接复用我们所编写的数组类 Array ,而是使用java所提供的数组,基于java的数组,使用2个标志来标示队列的首尾,因此使得在数组头插入数据的时间复杂度变成O(1)。

如果我们想使用链表来实现队列,不能直接使用我们创建的链表类 LinkedList 来实现,因为使用 LinkedList链表 直接实现队列,会导致 队列的增删操作必然有一个是O(n) 复杂度。因此,我们要使用 循环队列的思想 以及 链表的思想 来实现 链表类的队列 LinkedListQueue。
我们通过自己创建的 Node 结点,附加标志首尾的front与tail,这样就可以使用实现

//#include"LinkedList.h"
#pragma once
#include"Queue.h"
using namespace std;
#include <iostream>

template<typename T>
class Node {
public:
	T e;
	Node<T>* next;
	//Node(T e, Node *next) : e(e), next(next) {}

	//Node(T e) : e(e), next(nullptr) {}

	//Node() : e(), next(nullptr) {}

	Node(T e, Node* next) {
		this->e = e;
		this->next = next;
	}
	Node(T e) {
		this->e = e;
		this->next = nullptr;
	}
	Node() {
		this->e = NULL;
		this->next = nullptr;
	}
};

template<typename T>
class LinkedListQueue :public Queue<T> {
private:
	//LinkedList<T>* linkedlist;
	Node<T>* head;
	Node<T>* tail;
	int size;//这里的size要的,不管你linkedlist里有没有,
public:
	LinkedListQueue() {
		size = 0;
		head = nullptr;
		tail = nullptr;
	}
	int getSize() {
		return size;
	}
	bool isEmpty() {
		return size == 0;
	}
	void enqueue(T e) {
//当 tail=null,说明链表是空的
		if (tail == nullptr) {
//创建一个新的结点,并使得tail指向这个结点。那么这个结点暂时可以称为 tail
			tail = new Node<T>(e);
//同时注意,这个结点也是链表的第一个结点,那么我们使得指向链表第一个结点的指针head指向这个新结点 tail
			head = tail;
		}
		else {
			tail->next = new Node<T>(e);
			tail = tail->next;
		}		
		size++;//最后将链表长度加1
	}
	T dequeue() {
		if (size == 0 || head==nullptr) {
			cout << "isEmpty\n";
		}
		else {
// 出队方法:dequeue,从链表的头部出队,需要对head进行操作
			Node<T>* retNode = head;
			head = head->next;
			//cout << "head test:" << (head==nullptr) << endl;
			retNode->next = nullptr;
			if (head == nullptr) {
				tail = nullptr;
			}
			size--;
			return retNode->e;
		}
	}
	T getFront() {
		if (size == 0) {
			cout << "isEmpty";
		}
		else {
			return head->e;
		}
	}
	void print() const{
		Node<T> *cur = head;//形参不匹配说的就是这里要用cur来代替head,不然你改了head前面就nullprt了
		cout << "head:  ";
		while (cur != NULL) {
			
			cout << cur->e << "->";
			cur = cur->next;
			//cout << "  tail";
		}
		//cout << '\n';
		cout << "NULL" << "   size:" << size << endl;
	}
};
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值