数据结构和算法,是程序设计中重要的两大组成部分,我们的编程都是在选择和设计合适的数据结构存放数据,然后再用合适的算法处理这些数据。
链表是一种动态的数据结构,每次插入一个结点,只需为其分配内存然后保证节点指针域指向新的节点。
0.摘要
- 定义、插入节点、删除节点
- 6 反向遍历链表(栈、递归、反向迭代器)
- 18 删除链表的节点
- 22 输出链表中倒数第K个节点(遍历两次O(m+n)、滑动窗O(n)、栈)
- 23 链表中环的入口节点
- 24 反转链表(迭代、递归)双向链表反转
- 25 合并两个排序的链表(递归)
-
奇偶链表
- 35 复杂链表的复制(迭代)拆分为三小部分
- 36 二叉搜索树与双向链表
- 52 两个链表的第一个公共节点
1、定义、插入节点、删除节点
定义:
单向链表的节点包括:
- 数据域:用于存储数据元素的值。
-
指针域(链域):用于存储下一个结点地址或者说指向其直接后继结点的指针。
struct Node{
int value;
Node * next;
};
插入节点:
核心思想:创建新的节点new Node(),先对新节点的数据域和指针域赋值,再连接新节点和前面链表。
// 在第i个节点为止后插入值为x的节点
void SingleList::Insert(int i, int x) {
Node* p = head;
for (int j = 0; j <= i ; ++j) {//遍历到节点p位置
p = p->next;
}
Node *newNode = new Node();//创建新的节点
newNode->value = x;
newNode->next = p->next;
p->next = newNode;
}
删除节点:
// 删除第i个节点
void SingleList::Delete(int i) {
Node* p = head;
for (int j = 0; j <= i ; ++j) {//遍历到节点p位置
p = p->next;
}
Node* tmp=p->next;
p->next=p->next->next;
delete(tmp);
}
6、输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。
法一: 栈
反向遍历链表就类似于事先遍历的节点后输出,即“先进后出”,那么可以将链表遍历存放于栈中,其后遍历栈依次弹出栈节点,达到反向遍历效果。
vector<int> printListFromTailToHead(ListNode* head) {
stack<int> sta;
ListNode* p=head;
// 先遍历到指针p
while(p!=NULL)
{
sta.push(p->val);
p=p->next;
}
//利用栈,后进先出
vector<int> vec;
while(sta.size())
{
vec.push_back(sta.top());
sta.pop();
}
return vec;
}
递归
基本思想,是把规模较大的一个问题,分解成规模较小的多个子问题去解决,而每一个子问题又可以继续拆分成多个更小的子问题。
最重要的一点就是假设子问题已经解决了,现在要基于已经解决的子问题来解决当前问题;或者说,必须先解决子问题,再基于子问题来解决当前问题。
【A】----依赖---->【B】----依赖---->【C】
我们的终极目的是要解决问题A,那么三个问题的处理顺序如下:
开始处理问题A;
由于A依赖B,因此开始处理问题B;
由于B依赖C,开始处理问题C;
结束处理问题C;结束处理问题B;结束处理问题A
总结:栈的核心思想是递归,递归过程返回的顺序是前进顺序的逆序。
一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
递归多用 if,选择结构。每次都要判断,层层递进,每次都调用这个函数。看起来清晰但消耗空间大
int Fun(int n)
{
if(n==1)
{
return 1;
}
else
{
return n+Fun(n-1);
}
}
迭代用while ,循环结构。消耗空间小。
int sum=0,i=1;
while(i<=n)
{
sum+=i;
i++;
}
法二: 递归
vector<int> printListFromTailToHead(ListNode* head) {//输入是个指针(地址),返回一个容器vector
ListNode* p=head;
vector<int> vec;
if(p!=NULL)//第一步判断当下指针是否为空
{
if(p->next!=NULL)//第二步的下一步指向的指针不是空才能使用递归
{
vec=printListFromTailToHead(p->next);//递归要符合函数定义:有返回容器类型
}
vec.push_back(p->val);//前序遍历的判断,从最后向前执行
}
return vec;//满足函数定义返回类型
}
反向迭代器
从最后一个元素到第一个元素遍历容器,++访问前一个元素,--访问后一个元素。
法三:反向迭代器
vector<int> printListFromTailToHead(ListNode* head) {//输入是个指针(地址),返回一个容器vector
ListNode* p=head;
vector<int> vec;
while(p!=NULL)
{
vec.push_back(p->val);//按顺序放进容器
p=p->next;
}
return vector<int>(vec.rbegin(),vec.rend());//反向迭代器
}
18 删除链表中的节点
18.1 O(1)时间内删除链表单个节点
1、下一个节点 j 复制到 i
2、i的指针指向j的下一个节点
3、删除节点 j
void deleteNode(ListNode* node) {
//1 不是尾节点
if(node->next!=NULL){
ListNode* temp=node->next;//下一个指针j
node->val=temp->val;// 下一个指针j内容复制到i
node->next=temp->next;// i的指针指向j的下一个节点
delete temp;//删除节点j
temp=NULL//避免成为野指针
}
//2 只有一个节点
else if(node==pListHead){
delete node;
node=NULL
pListHead=NULL;
}
//3 是尾节点
else
{
ListNode* pNode=pListHead;
while(pNode->next!=NULL){
pNode=pNode->next;
}
pNode->next=NULL;
delete node;
node=NULL;
}
}
第三种情况:是尾节点,所以没有下一个节点。前一个节点将指向NULL,获得前一个节点只能从头遍历。
18.2 排序的链表中删除重复的节点
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
class Solution {
public:
ListNode* deleteDuplication(ListNode* pHead)
{
if(pHead==NULL)
return NULL;
// 创建新节点
ListNode* Head=new ListNode(-1);
Head->next=pHead;// Head要一直指向最开始的头,保持不变
ListNode* pre=Head;// pre指向当前确定不重复节点
ListNode* p=pHead;//用p工作指针进行遍历更改
while(p!=NULL&&p->next!=NULL){//循环
if(p->next->val==p->val){//出现重复
int value=p->val;//重复的节点有相同数值
while(p!=NULL&&p->val==value)
p=p->next;//至不相同的数
pre->next=p;//需要修改链接。
}
else{//没有重复,pre和p都向下走就行,没有链接需要修改。
pre=p;
p=p->next;
}
}
return Head->next;//返回头结点的下一节点
}
};
思路:添加一个新节点,
- 新建一个空的头节点,因为这里面牵扯到换新的链表头的问题,所以为了方便新建一个新的节点作为链表的头节点
同时解决了头结点就是重复值的麻烦。
更改原来的链表主要指的是更改原来链表的链接方式。
22、输入一个链表,输出该链表中倒数第k个结点。
法一:遍历两次 O(m+n)
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
ListNode* p=pListHead;
//节点数量,头指针也算一个
int count=0;
while(p!=NULL)//空指针
{
p=p->next;
count++;
}
//特殊情况需要考虑,否则通不过
if(k>count)
{
return NULL;
}
//输出
ListNode* p1=pListHead;
for(int i=0;i<count-k;i++)
{
p1=p1->next;
}
return p1;//返回节点
}
法二:滑动窗 O(n)
快慢两指针一起走,遍历一次即可。
注意鲁棒性:空指针、k=0、count小于 k。
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
ListNode* p1=pListHead;
ListNode* p2=pListHead;
if(pListHead==nullptr||k==0)//判断是否空指针或k=0
return NULL;
for(int i=0;i<k-1;i++)
{
if(p2->next!=NULL)//至关重要:判断没有到末尾
{
p2=p2->next;
}
else
return NULL;
}
while(p2->next!=NULL)
{
p1=p1->next;
p2=p2->next;
}
return p1;
}
new动态内存分配
出现情况:动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配。
C++中,创建动态数组比较容易,只要将数组的数据类型和元素数目告诉给new即可。
new运算符返回的是一个指向所分配类型变量的指针。格式如下:
int *psome=new int [10] () ; //十个值初始化为0的int,()表示进行值初始化
delete [] psome; //对应后面删除这个数组,[]表示释放的整个数组,否则就只是指针指向的元素。
这里类指针需要new分配内存,stack<ListNode*> *sta=new stack<ListNode*>()
法三:栈(通不过问什么???)
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
if(pListHead==nullptr||k==0)//判断是否空指针或k=0
return NULL;
ListNode* p=pListHead;//类指针
stack<ListNode*> *sta=new stack<ListNode*>();//new需要类指针,栈中成员类型为ListNode*
int count=0;
while(p->next!=NULL)
{
sta->push(p);
count++;
p=p->next;
}
if(count<k)
return NULL;
ListNode* p1=nullptr;
for(int i=0;i<k;i++)
{
p1=sta->pop();
}
return p1;
}
23 链表中环的入口节点
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
1、设置快慢指针,假如有环,他们最后一定相遇。
2、两个指针分别从链表头和相遇点继续出发,每次走一步,最后一定相遇与环入口。
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
//特殊:空链表
if(pHead==nullptr)
return NULL;
ListNode* fast=pHead;
ListNode* slow=pHead;
//循环
while(fast&&fast->next){//有环的话,不会到尾节点
fast=fast->next->next;//fast速度是slow的两倍
slow=slow->next;
//判断有环
if(slow==fast){//相遇则有环,要找入口节点
slow=pHead;//slow从头开始
while(slow!=fast){//没有二次相遇,
slow=slow->next;//现在都是1的速度
fast=fast->next;
}
return slow;
}
}
return NULL;//没有环
}
};
24、输入一个链表,反转链表后,输出新链表的表头。
法一:pre、pNode、next
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
if(pHead==NULL)
return NULL;//程序鲁棒性
ListNode* pre=nullptr;
ListNode* pNode=pHead;
ListNode* next=nullptr;
while(pNode!=NULL)
{
next=pNode->next;//保存改变方向之前的next
pNode->next=pre;//改变方向
//更新pre和pNode
pre=pNode;
pNode=next;
}
return pre;//如果pNode为null的时候,pre就为最后一个节点了
}
};
法二:递归
它利用递归走到链表的末端,然后再更新每一个node的next 值 ,实现链表的反转。
ListNode* ReverseList(ListNode* pHead) {
ListNode* p=pHead;
//如果链表为空或者链表中只有一个元素
if(p==NULL||p->next==NULL)
return p;
else{
//先反转后面的链表,走到链表的末端结点
ListNode* pReverseNode=ReverseList(p->next);
//再将当前节点设置为后面节点的后续节点
p->next->next=p;
p->next=NULL;
return pReverseNode;
}
}
双向链表反转
class Solution {
public:
DoubleNode* ReverseList(DoubleNode* pHead) {
if(pHead==NULL)
return NULL;//程序鲁棒性
DoubleNode* pre=nullptr;
DoubleNode* pNode=pHead;
DoubleNode* next=nullptr;
while(pNode!=NULL)
{
next=pNode->next;//保存改变方向之前的next
pNode->next=pre;//改变方向
pNode->pre=next;//多了一步前置处理
//更新pre和pNode
pre=pNode;
pNode=next;
}
return pre;//如果pNode为null的时候,pre就为最后一个节点了
}
};
25、输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
法一:递归
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
//如果其中一个是空链表,返回另外一个链表的头指针
if(pHead1==NULL)
{
return pHead2;
}
else if(pHead2==NULL)
{
return pHead1;
}
ListNode* result=NULL;
if(pHead1->val<pHead2->val)//这里用if还是while???递归应该是一次性的if,pHead1与pHead2
{
result=pHead1;
result->next=Merge(pHead1->next,pHead2);
}
else
{
result=pHead2;
result->next=Merge(pHead1,pHead2->next);
}
return result;
}
};
法二:非递归
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
if(pHead1==NULL)
return pHead2;
if(pHead2==NULL)
return pHead1;
//0. 新建头节点pHead
ListNode* pHead=new ListNode(0);
ListNode* p=pHead;
// 1.两链表都有数,没有一个是到结尾
while(pHead1!=NULL&&pHead2!=NULL){
if(pHead1->val<pHead2->val){
p->next=pHead1;//p的下一节点数值
pHead1=pHead1->next;//更新list1
}
else{
p->next=pHead2;
pHead2=pHead2->next;
}
p=p->next;//更新当前节点p
}
// 2.有一个到底了
if(pHead1==NULL)
p->next=pHead2;
if(pHead2==NULL)
p->next=pHead1;
// 3.返回第一个节点
return pHead->next;
}
};
思想:ListNode* p=pHead; 然后p来进行更新链表,pHead会一直指向头指针。最后返回的时候返回pHead。
Leetcode(链表)奇偶链表(c++)
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
示例 1:
输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL
示例 2:
输入: 2->1->3->5->6->4->7->NULL
输出: 2->3->6->7->1->5->4->NULL
说明:
应当保持奇数节点和偶数节点的相对顺序。
链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
class Solution {
public:
ListNode* oddEvenList(ListNode* head) {
if(!head||!head->next||!head->next->next) return head;
ListNode*pt1=head,*pt2=head->next,*pt3=head->next;
ListNode*temp;
while(pt2&&pt2->next){ //pt2->next==null或者pt2==NULL,这两种情况分别是链长为偶数长和奇数长的break条件。可以写到一起。
temp=pt2->next; //先保留pt2->next;
// 偶 //
pt2->next=pt2->next->next;
pt2=pt2->next; // 更新pt2
// 奇 //
pt1->next=temp;
pt1=pt1->next; // 更新pt1
// 奇的尾部链接偶头
temp->next=pt3;
}
return head;
}
};
35 复杂链表的复制
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
class Solution {
public:
RandomListNode* Clone(RandomListNode* pHead)
{
if(pHead==NULL)
return NULL;
CloneNodes(pHead);//复制链表节点,插到原节点后方
ConnectSibling(pHead);//新节点的随机指针指向
return reconnect(pHead);//区分奇偶链表
}
void CloneNodes(RandomListNode* pHead){
RandomListNode* pNode=pHead;
while(pNode!=NULL){//循环
RandomListNode* pCloned=new RandomListNode(pNode->label);//新建节点
pCloned->next=pNode->next;
pNode->next=pCloned;//更新pNode
pNode=pCloned->next;
}
}
void ConnectSibling(RandomListNode* pHead){
RandomListNode* pNode=pHead;
while(pNode!=NULL){//循环
RandomListNode* pCloned=pNode->next;//更新pCloned和pNode
if(pNode->random!=NULL){
pCloned->random=pNode->random->next;//关键思想
}
pNode=pCloned->next;
}
}
RandomListNode* reconnect(RandomListNode* pHead){
RandomListNode* pNode=pHead;
RandomListNode* result=pHead->next;//指向复制的链表的头指针
while(pNode!=NULL){
RandomListNode* pCloned=pNode->next;//更新pCloned
pNode->next=pCloned->next;//更新pNode
pNode=pNode->next;
if(pNode!=NULL)//更新的pNode不为0
pCloned->next=pNode->next;
}
return result;
}
};
36 二叉搜索树与双向链表
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
// 中序遍历是递增的,左根右。
class Solution {
public:
TreeNode* Convert(TreeNode* pRootOfTree)
{
if(pRootOfTree==NULL)
return NULL;
TreeNode* list=nullptr;
// 主函数,list指向双向链表的尾节点
func(pRootOfTree,&list);
// 函数后list指向尾部,需要向前遍历到头节点
TreeNode* result=list;
while(result->left)
result=result->left;
return result;
}
void func(TreeNode* pNode,TreeNode** list){//题目中要求不得创建新结点,通过操作二级指针达到创建结点的目的
if(pNode==NULL)
return;
TreeNode* pCurrent=pNode;
// 左根右
if(pCurrent->left)//左子树
func(pCurrent->left,list);
// 根
pCurrent->left=*list;//当前节点左指针指向转换好的链表最后一个位置
if(*list!=NULL)
(*list)->right=pCurrent;//转换好的链表最后一个节点右指针指向当前节点
*list=pCurrent;//更新链表最后一个节点
// 右
if(pCurrent->right)//右子树
func(pCurrent->left,right);
}
};
PS:对于这里双重指针的用法不理解???
52 两个链表的第一个公共节点
输入两个链表,找出它们的第一个公共结点。
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
//1.得到两个链表长度
int len1=getlength(pHead1);
int len2=getlength(pHead2);
int length=len1-len2;
// 2. 长的先走几步
if(len1>len2)
pHead1=walk(pHead1,length);
else
pHead2=walk(pHead2,length);
// 3. 剩下的比较,找到的话就返回,否则表示没有返回NULL
while(pHead1!=NULL){
if(pHead1==pHead2)
return pHead1;
pHead1=pHead1->next;
pHead2=pHead2->next;
}
return NULL;//找不到返回NULL
}
//获得长度
int getlength(ListNode* pHead)
{
int length=0;
ListNode* pNode=pHead;
while(pNode!=NULL)
{
length++;
pNode=pNode->next;
}
return length;
}
// 向后走几步
ListNode* walk(ListNode* pHead,int length){
while(length--){
pHead=pHead->next;
}
return pHead;
}
};
思路:核心是一旦找到相同的节点,剩下的就是重叠的。所以应该是Y型链表。最后尾节点在同一个位置。
问题转化为:让长的先走几步至长度相同,然后再齐头并进开始比较对应的节点。