c++ 数据结构和算法之刷无聊的面试题(1)-单链表


(一)链表节点ListNode类~

template<typename T>
struct ListNode
{
	T _data;
	ListNode<T>* _next;
	ListNode() :_data(0), _next(nullptr)
	{}
};

(二)单链表MyForward_List类~

(在下面再写出其中的函数,方便起见,不用分文件编写的方式),所有函数方便起见,全部设定为public

template<typename T>
class MyForward_List
{
private:
	ListNode<T>* _head;
	int _size;//链表的大小
};

1.构造函数 MyForward_List()~

注意:鉴于我目前看到的c++链表面试题的解法,基本头节点都不是空哨兵节点,所以此处,只要链表不为空,_head均有意义,并非是空哨兵节点(即_head的data没意义,也不会去访问,其只有一个next指针指向链表首节点)。(实际上最好有一个空哨兵作为首节点,如果有空哨兵,有的函数能写的更加简便点)

	MyForward_List()
	{
		_head = nullptr;
		_size = 0;
	}

2.删除链表中所有数据的函数 clear()~

由于所有数据在插入时,都会采用在堆区创建的形式,所以需要一个函数来清空所有堆区的数据,因此也需要遍历一遍单链表。

	/*清楚所有数据*/
	void clear()
	{//要遍历一遍,将所有数据删除。
		ListNode<T>* next = nullptr;//当前要被删除节点的后继节点
		for (ListNode<T>* curr = _head; curr != nullptr;)
		{
			next = curr->_next;
			delete curr;
			curr = next;
		}
		_size = 0;
	}

3.析构函数 ~MyForward_List()~

	~MyForward_List()
	{
		clear();
	}

4.头插法插入元素 push_front(T data)~

	void push_front(T data)
	{
		if (_head == nullptr)//如果头节点为空,则将此节点设为头节点
		{
			_head = new ListNode<T>();
			_head->_data = data;
			_head->_next = nullptr;
		}
		else
		{
			ListNode<T>* oldHead = new ListNode<T>();//创建新节点,其值为原head的值。
			oldHead->_data = _head->_data;
			oldHead->_next = _head->_next;

			_head->_data = data;
			_head->_next = oldHead;
		}
		_size++;
	}

5.头删法删除元素 pop_front()~

任何删除函数,都需要考虑到链表是否为空的情况。

	void pop_front()
	{
		if (_head != nullptr)
		{
			ListNode<T>* headNext = _head->_next;
			delete _head;
			_head = headNext;
			_size--;
		}
	}

6.尾插法插入元素 push_back(T data)~

尾插法个人认为对于单链表的实际使用中,没有任何意义(你真想要尾插,可以用双链表啊)

	void push_back(T data)
	{
		if (_head == nullptr)//如果头节点为空,则将此节点设为头节点
		{
			_head = new ListNode<T>();
			_head->_data = data;
			_head->_next = nullptr;
		}
		else
		{
			ListNode<T>* curr = _head;
			for (; curr->_next != nullptr; curr = curr->_next);//找到尾结点,此时其next一定为空

			ListNode<T>* newNode = new ListNode<T>();
			curr->_next = newNode;
			newNode->_data = data;
			newNode->_next = nullptr;
		}
		_size++;
	}

7.尾插法删除元素 pop_back()~

同理,尾删法,对于单链表,也是没有意义的。但注意,真要写的时候,需要两个辅助的指针,才能很好地实现尾删。

	/*尾删*/
	void pop_back()
	{
		if (_head != nullptr)
		{
			ListNode<T>* curr = _head, * prev = _head;
			for (; curr->_next != nullptr; prev = curr, curr = curr->_next);//找到尾结点,需要额外的节点prev来作为curr的前驱,否则curr删除后,其前驱的next指向的是野指针。
			delete curr;
			prev->_next = nullptr;
			curr = nullptr;
			_size--;
		}
	}

8.按data删除链表中其中一个元素 erase(T data)

基本也要两个指针辅助删除。

	bool erase(T data)//删除一个元素。
	{
		if (_head == nullptr)
		{
			return false;
		}
		else if (_head->_data == data)//看头节点是否是要被删除的元素
		{
			ListNode<T>* headNext = _head->_next;
			delete _head;
			_head = headNext;
			_size--;
			return true;
		}

		for (ListNode<T>* prev = _head, *curr = _head->_next; curr != nullptr; prev = curr, curr = curr->_next)
		{
			if (curr->_data == data)
			{
				prev->_next = curr->_next;
				delete curr;
				curr = nullptr;
				_size--;
				return true;
			}
		}
		return false;
	}

9.删除符合data值的所有元素 remove(T data)

思路跟8差不多,就是需要对删除的这个元素,是不是头节点,进行区分。
理解的话,可以自行画图理解(比如 1->1->2删除1)(1->2->2删除2)

	void remove(T data)//删除符合这个条件的所有元素
	{
		while (_head != nullptr && _head->_data == data)//看头节点是否是要被删除的元素,也要判空
		{
			ListNode<T>* headNext = _head->_next;
			delete _head;
			_head = headNext;
			_size--;
		}
		if (_head == nullptr)//到这一步的条件为头节点为空,或者头节点的值不等于data
		{
			return;
		}
		for (ListNode<T>* curr = _head->_next, *prev = _head; curr != nullptr; )//这一步的条件为头节点的值不等于data
		{
			if (curr->_data == data)
			{
				prev->_next = curr->_next;
				delete curr;
				curr = prev->_next;
				_size--;
			}
			else
			{
				prev = curr;
				curr = curr->_next;
			}
		}
	}

10.找到倒数第k个节点-解法1 Find_K_Node(const int& k)

由于在定义链表的时候,我们定义了_size这个成员变量,所以直接利用这个变量。

	//找倒数第k个节点
	ListNode<T>* Find_K_Node(const int& k)const
	{
		if (k<1 || k>_size)
		{
			return nullptr;
		}
		int number = 0;
		int length = _size - k + 1;//对应倒数第k个的正数。
		for (ListNode<T>* curr = _head; curr != nullptr; curr = curr->_next)
		{
			if (++number == length)
			{
				return curr;
			}
		}
		return nullptr;//只有当_head=nullptr时,会出现这种情况。
	}

11.找到倒数第k个节点-解法2 Find_K_Node2(const int& k)

此法为不借助size的解法,也是网上常规的,利用两个指针的解法。

当第一个指针达到k-1个位置的时候,第二个指针开始出发,当第一个指针到达尾节点的时候,第二个指针此时的节点,就是倒数第k个节点。

	//找倒数第k个节点//不借助size
	ListNode<T>* Find_K_Node2(const int& k)const
	{
		if (k < 1 || _head == nullptr)//保证不越界,以及链表为空的情况
		{
			return nullptr;
		}
		int number = 0;
		ListNode<T>* KNode = nullptr, * curr = nullptr;
		for (curr = _head; curr->_next != nullptr; curr = curr->_next)//这里需要改成curr->next不能为空
		{
			++number;
			if (number == k - 1)
			{
				KNode = _head;//当第一个指针指的位置离head有k-1个时,第二个指针就可以开始出发了。
			}
			else if (number > k - 1 && KNode != nullptr)//当第二个指针可以出发时
			{
				KNode = KNode->_next;
			}
		}
		if (k == 1)//如果是倒数第一个
		{
			return curr;
		}
		if (KNode != nullptr)//如果第二个指针不为空,则返回第二个指针。
		{
			return KNode;
		}
		return nullptr;//当_head=nullptr时,或者KNode为nullptr时,会出现这种情况。
	}

12.反转链表 reverse()

看似麻烦,实际上,画个图就能解决,首先反转,必须要遍历整个链表,在遍历过程中完成反转。因此,可以试着画(1->2->3)进行反转和(1->2->3->4)进行反转。思路就会清晰很多。

在下列函数中,用到了3个指针,分别指向当前节点的前驱,当前节点,当前节点的后继。以完成链表的反转。

并要对头节点进行额外的考虑。

	//反转链表
	void reverse()//画图1 2 3 4进行分析就能明白,需要3个指针进行辅助。
	{
		if (_head == nullptr)
		{
			return;
		}
		ListNode<T>* curr = _head->_next, * prev = _head, * next = _head->_next;
		for (; curr != nullptr; prev = curr, curr = next)
		{
			next = curr->_next;
			curr->_next = prev;
			if (prev == _head)
			{
				prev->_next = nullptr;
			}
		}//循环完之后,此时curr为nullptr,prev为尾结点
		_head = prev;//就更新头节点。只有一个节点时,这个也成立
	}

13.正向输出单链表 PrintList()

	/*遍历输出*/
	void PrintList()const
	{
		for (ListNode<T>* curr = _head; curr != nullptr; curr = curr->_next)
		{
			cout << curr->_data << endl;
		}
		cout << "size 为" << _size << endl;
	}

14.反向遍历输出单链表 PrintReverseList()

此时,只需要一个辅助栈,就能解决反向输出的问题

	//反向遍历输出
	void PrintReverseList()const
	{
		stack<T> DataStack;
		for (ListNode<T>* curr = _head; curr != nullptr; curr = curr->_next)
		{
			DataStack.push(curr->_data);
		}
		while (!DataStack.empty())
		{
			cout << DataStack.top() << endl;
			DataStack.pop();
		}
		cout << "size 为" << _size << endl;
	}

其中empty()函数为

	bool empty()const
	{
		return !_size;
	}

15.获取链表的首元素 front()

这个会在下面的合并两个排序的链表,以及找两个链表公共的节点中用到。

实际上这个函数并不规范,在stl的list中,front()成员函数是用来得到节点的data(ValueType)值的。在此处为了方便起见,可以直接得到_head。(实际上,省事点,将_head设为public就行了)

	//首元素//实际上这个不是很规范
	ListNode<T>*& front()
	{
		return _head;
	}

16.得到链表的长度 getSize()

	int getSize()const
	{
		return _size;
	}

(三)非成员函数骚操作链表题

1.合并两个排序的链表

思路就是归并排序的归并操作。懂归并排序的归并原理,这个也就懂了。

//合并两个排序的链表,假设都是从小到大排列
template<typename T>
void MergeSortedList(MyForward_List<T>& L1, MyForward_List<T>& L2, MyForward_List<T>& L)
{
	ListNode<T>* L1Node = L1.front();
	ListNode<T>* L2Node = L2.front();
	while (L1Node != nullptr && L2Node != nullptr)
	{
		if (L1Node->_data < L2Node->_data)
		{
			L.push_front(L1Node->_data);
			L1Node = L1Node->_next;
		}
		else
		{
			L.push_front(L2Node->_data);
			L2Node = L2Node->_next;
		}
	}
	while (L1Node != nullptr)
	{
		L.push_front(L1Node->_data);
		L1Node = L1Node->_next;
	}
	while (L2Node != nullptr)
	{
		L.push_front(L2Node->_data);
		L2Node = L2Node->_next;
	}
	L.reverse();//反转链表,因为是按头插的,顺序会改变。
}

2.找两个链表最近公共的节点解法1

一般,是这样的骚链表,才会出现这种情况(这里用到了其他老兄的一张图)
在这里插入图片描述

第一种利用辅助栈,进行解决,相对蛮力的方式。将两个链表从头到尾遍历一遍,将结果存到两个栈中,再从两个栈顶分别弹出元素,第一个不相等的元素的前一个栈顶元素,就必然是两个链表的最近公共节点。

原理方面应该没问题,个人没有测试这段代码,应该是没问题的,有问题可以下方留言。

template<typename T>
ListNode<T>* FirstCommonNode1(MyForward_List<T>& L1, MyForward_List<T>& L2)//不写测试代码,太麻烦//栈的方式也太繁琐,不用
{
	ListNode<T>* L1Node = L1.front();
	ListNode<T>* L2Node = L2.front();
	if (L1Node == nullptr || L2Node == nullptr)
	{
		return nullptr;
	}
	stack<ListNode<T>*> L1Stack;
	stack<ListNode<T>*> L2Stack;
	//将L1和L2的data 数据存到stack中
	for (ListNode<T>* curr = L1Node; curr != nullptr; curr = curr->_next)
	{
		L1Stack.push(curr);
	}

	for (ListNode<T>* curr = L2Node; curr != nullptr; curr = curr->_next)
	{
		L2Stack.push(curr);
	}

	ListNode<T>* CommonNode = nullptr;
	while (L1Stack.top() == L2Stack.top())//当栈顶元素不一致时,退出循环
	{
		CommonNode = L1Stack.top();
		L1Stack.pop();
		L2Stack.pop();
	}
	return CommonNode;
}

3.找两个链表最近公共的节点解法2

这里用到了这个老哥(https://www.cnblogs.com/kevinsharif/p/9216807.html)的第七个部分的内容的思想。即将链表长度长的链表先遍历到跟短的链表一样的长度,再一个个比较。第一个相同的节点,就必然是两个链表的最近公共节点。

template<typename T>
ListNode<T>* FirstCommonNode2(MyForward_List<T>& L1, MyForward_List<T>& L2)//不写测试代码,太麻烦//减长度的方式更高效
{
	ListNode<T>* L1Node = L1.front();
	ListNode<T>* L2Node = L2.front();
	if (L1Node == nullptr || L2Node == nullptr)
	{
		return nullptr;
	}
	int L1Size = L1.getSize();
	int L2Size = L2.getSize();

	int diff = L1Size - L2Size;
	if (diff > 0)//选择最长的那个,“剪去”多余的长度
	{
		for (ListNode<T>* curr = L1Node; diff > 0; diff--)
		{
			curr = curr->_next;
		}
	}
	else
	{
		diff = -diff;
		for (ListNode<T>* curr = L2Node; diff > 0; diff--)
		{
			curr = curr->_next;
		}
	}
	//到这一步,两个链表的长度应该一致。
	for (ListNode<T>* curr = L1Node, *curr2 = L2Node; curr != nullptr; curr = curr->_next, curr2 = curr2->_next)
	{
		if (curr == curr2)//只要碰到一个相等的,就返回。
		{
			return curr;
		}
	}
	return nullptr;//说明两个链表没有公共的节点
}

(四)后序可能添加的

网上看到一些骚题,比如判断一个单链表是否有环,环中节点个数,环入口等等题,有时间我再看看并进行总结,虽然其他人也总结的比较多也比较好了。

说实话,个人认为这些真心没意义,除了面试或者考试只能考这些以外。= =。不过,如果你分析过二叉树,红黑树等的写法的话,再来看链表的这些骚操作题,哪怕跟我一样第一次看,也能很快理解和掌握。然后为了面试再把这些背下来(o(╯□╰)o)。

对我写的代码不是很懂的,可以在下方留言,有必要的话,我再进行更多的注解实际上对于这种指针题,画图就能解决大部分问题!剩下就看悟性和数学功底了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值