赞
踩
template<typename T>
struct ListNode
{
T _data;
ListNode<T>* _next;
ListNode() :_data(0), _next(nullptr)
{}
};
(在下面再写出其中的函数,方便起见,不用分文件编写的方式),所有函数方便起见,全部设定为public
template<typename T>
class MyForward_List
{
private:
ListNode<T>* _head;
int _size;//链表的大小
};
注意:鉴于我目前看到的c++链表面试题的解法,基本头节点都不是空哨兵节点,所以此处,只要链表不为空,_head均有意义,并非是空哨兵节点(即_head的data没意义,也不会去访问,其只有一个next指针指向链表首节点)。(实际上最好有一个空哨兵作为首节点,如果有空哨兵,有的函数能写的更加简便点)
MyForward_List()
{
_head = nullptr;
_size = 0;
}
由于所有数据在插入时,都会采用在堆区创建的形式,所以需要一个函数来清空所有堆区的数据,因此也需要遍历一遍单链表。
/*清楚所有数据*/
void clear()
{//要遍历一遍,将所有数据删除。
ListNode<T>* next = nullptr;//当前要被删除节点的后继节点
for (ListNode<T>* curr = _head; curr != nullptr;)
{
next = curr->_next;
delete curr;
curr = next;
}
_size = 0;
}
~MyForward_List()
{
clear();
}
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++; }
任何删除函数,都需要考虑到链表是否为空的情况。
void pop_front()
{
if (_head != nullptr)
{
ListNode<T>* headNext = _head->_next;
delete _head;
_head = headNext;
_size--;
}
}
尾插法个人认为对于单链表的实际使用中,没有任何意义(你真想要尾插,可以用双链表啊)
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++; }
同理,尾删法,对于单链表,也是没有意义的。但注意,真要写的时候,需要两个辅助的指针,才能很好地实现尾删。
/*尾删*/
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--;
}
}
基本也要两个指针辅助删除。
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; }
思路跟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; } } }
由于在定义链表的时候,我们定义了_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时,会出现这种情况。 }
此法为不借助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时,会出现这种情况。 }
看似麻烦,实际上,画个图就能解决,首先反转,必须要遍历整个链表,在遍历过程中完成反转。因此,可以试着画(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;//就更新头节点。只有一个节点时,这个也成立 }
/*遍历输出*/
void PrintList()const
{
for (ListNode<T>* curr = _head; curr != nullptr; curr = curr->_next)
{
cout << curr->_data << endl;
}
cout << "size 为" << _size << endl;
}
此时,只需要一个辅助栈,就能解决反向输出的问题
//反向遍历输出
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;
}
这个会在下面的合并两个排序的链表,以及找两个链表公共的节点中用到。
实际上这个函数并不规范,在stl的list中,front()成员函数是用来得到节点的data(ValueType)值的。在此处为了方便起见,可以直接得到_head。(实际上,省事点,将_head设为public就行了)
//首元素//实际上这个不是很规范
ListNode<T>*& front()
{
return _head;
}
int getSize()const
{
return _size;
}
思路就是归并排序的归并操作。懂归并排序的归并原理,这个也就懂了。
//合并两个排序的链表,假设都是从小到大排列 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();//反转链表,因为是按头插的,顺序会改变。 }
一般,是这样的骚链表,才会出现这种情况(这里用到了其他老兄的一张图)
第一种利用辅助栈,进行解决,相对蛮力的方式。将两个链表从头到尾遍历一遍,将结果存到两个栈中,再从两个栈顶分别弹出元素,第一个不相等的元素的前一个栈顶元素,就必然是两个链表的最近公共节点。
原理方面应该没问题,个人没有测试这段代码,应该是没问题的,有问题可以下方留言。
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; }
这里用到了这个老哥(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)。
对我写的代码不是很懂的,可以在下方留言,有必要的话,我再进行更多的注解。实际上对于这种指针题,画图就能解决大部分问题!剩下就看悟性和数学功底了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。