文章目录
参考左神上课所讲内容,所使用语言为C++
1.单链表的基本操作
包含单链表的创建(头插法,尾插法),链表打印,删除链表结点等操作,要注意边界条件
list.h
#ifndef _LIST_H_
#define _LIST_H_
#include <iostream>
using namespace std;
typedef int ElementType;//数据类型
class Node{
public:
Node(){
next = NULL;}
ElementType data;
Node* next;
};
class List{
public:
List();
virtual ~List();
/*链表相关操作*/
void insertHead(ElementType data);//头插法
void insertTail(ElementType data);//尾插法
bool insertNext(Node* node,ElementType data);//把数据插入到结点node的后面
bool deleteNode(Node* node);//删除结点
void displayList();//从头打印链表元素
void reverse();//反转链表
Node* Find(int num);//返回第num个结点
Node* Search(ElementType data);//查找数据为data的结点
/*获取链表长度和头尾结点方法*/
int listLengh(){
return size;}
Node* headNode(){
return pHead;}
Node* tailNode(){
return pTail;}
private:
size_t size;
Node* pHead;//头结点指针
Node* pTail;//尾节指针
};
#endif
list.cpp
#include "list.h"
typedef int ElementType;//数据类型
List::List(){
size = 0;
pHead = new Node();
pTail = pHead;
}
List::~List(){
while(size > 0){
deleteNode(pHead);
}
delete pHead;
pHead = NULL;
}
/*链表相关操作*/
void List::insertHead(ElementType data){
Node* node = new Node();
node->data = data;
if(size == 0)
pTail = node;
node->next = pHead->next;
pHead->next = node;
size++;
}
void List::insertTail(ElementType data){
Node* node = new Node();
node->data = data;
node->next = pTail->next;///
pTail->next = node;
pTail = pTail->next;
size++;
}
bool List::insertNext(Node* node,ElementType data){
//把数据插入到结点node的后面
if(node == NULL)
return false;
Node* p = new Node();
p->data = data;
p->next = node->next;
node->next = p;
if(node == pTail)//插入的结点为尾部时,移动尾部指针
pTail = p;
size++;
return true;
}
bool List::deleteNode(Node* node){
//删除结点node后面的结点
if(NULL == node || NULL == node->next)
return false;
Node *p = node->next;
node->next = node->next->next;
delete p;
size--;
return true;
}
void List::displayList(){
//从头打印链表元素
Node* p = pHead->next;
while(p != NULL){
cout << p->data << " " ;
p = p->next;
}
cout << endl;
}
void List::reverse(){
//反转链表
if(size < 2)
return;
//pre是p的前序结点,post是p的后序结点
Node* pre = pHead->next;
Node* p = pre->next;
Node* post = NULL;
//处理尾部结点
pTail = pre;
pTail->next = NULL;
while(p != NULL){
post = p->next;
p->next = pre;
pre = p;
p = post;
}
//处理头结点
pHead->next = pre;
}
Node* List::Find(int num){
//返回第num个结点
if(num < 0 || num > size)
return NULL;
Node* p = pHead;
for(int i = 0;i < num;i++)
p = p->next;
return p;
}
Node* List::Search(ElementType data){
//查找数据为data的结点
if(size > 0){
Node* p = pHead->next;
while(p != NULL){
if(p->data == data)
return p;
p = p->next;
}
}
return NULL;
}
main.cpp
#include "list.h"
#include <iostream>
using namespace std;
int main(){
List* list = new List();
list->insertTail(1);
list->insertTail(2);
list->insertTail(3);
list->insertTail(4);
list->insertTail(5);
list->insertTail(6);
list->insertHead(7);
list->insertHead(8);
list->insertHead(9);
list->displayList();
list->deleteNode(list->Find(3));
list->displayList();
list->reverse();
list->displayList();
}
2. 链表相关问题
【笔试】一切为了时间复杂度,不要态在乎空间复杂度
【面试】时间复杂度依然放在第一位,但是一定要找到空间复杂度最省的方法
【技巧】
- 额外数据结构记录
- 快慢指针
2.1 反转单向链表
【题目】实现反转单向链表的函数
【要求】如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度为O(1)
注意是否需要返回头指针
2.1.1 反转单链表
方法一:利用有限个指针。空间复杂度O(1)
方法二:利用栈。空间复杂度O(N)
- 时间复杂度O(N),空间复杂度O(1)
//反转单链表
Node* reverseList(Node* head){
if(head->next == NULL || head->next->next == NULL)//链表为空或只有一个数
return NULL;
Node* ret = head;//用来保存头结点,如果头结点是链表数据,可不要此步和最后一步
head = head->next;
Node* pre = NULL;
Node* next = NULL;
while(head != NULL){
next = head->next;
head->next = pre;
pre = head;
head = next;
}
ret->next = pre;//把头结点重新插到头部
return ret;
}
方法二:利用栈
Node* reverseList2(Node* head){
Node* p = head->next;
stack<Node*> sta;
while(p != NULL){
sta.push(p);
p = p->next;
}
Node* q = head;
while(!sta.empty()){
q->next = sta.top();
q = q->next;
sta.pop();
}
q->next = NULL;//注意此处,一定要把尾指针置为NULL,否则会陷入死循环
return head;
}
2.2 对链表进行排序
操作同数组类似
2.3 打印两个有序链表的公共部分
【题目】给定两个有序链表的头指针head1和head2,打印两个链表的公共部分
【要求】如果两个链表的长度之和为N,时间复杂度要求O(N),额外空间复杂度为O(1)
定义两个指针,谁小谁动,相同打印,类似于merge的过程
【思路】因为是有序的,遍历两个列表,结点值相等时打印,不相等时谁小谁移动
void printSameNode(Node* head1,Node* head2){
if(head1 == NULL || head2 == NULL)
return ;
Node* cur1 = head1->next;
Node* cur2 = head2->next;
while(cur1 != NULL && cur2 != NULL){
if(cur1->data < cur2->data)
cur1 = cur1->next;
else if(cur1->data > cur2->data)
cur2 = cur2->next;
else{
cout << cur1->data << " ";
cur1 = cur1->next;
cur2 = cur2->next;
}
}
cout << endl;
return ;
}
2.4 回文结构
【题目】给定一个单链表的头结点,请判断该链表是否为回文结构
【要求】如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度为O(1)
【例子】1->2->2->1 true ;1->2->1 true ;1->2->3 false
【解题思路】
- 栈
- 把链表的全部内容依次放入栈中,再依次弹出栈中元素,并与链表依次进行比较,如果都相等,那么为回文结构,否则不是
- 时间复杂度:O(N)
- 空间复杂度:O(N)
- 快慢指针 + 栈
- 快指针一次走两步,慢指针一次走一步,当快指针走到结尾时,慢指针走到一半,然后把满指针后面的部分,放到栈中
- 时间复杂度:O(N)
- 空间复杂度:O(N/2)
- 有限个指针
- 改变链表右侧区域,使整个右半区域反转,最后指向中间结点;利用指针分别从两端移动,每移动一次比较两值是否一样。最后把链表恢复成原来的样子
- 空间复杂度O(1)
栈实现
bool isPalindrome1(Node* head){
Node *p = head->next;
stack<int> sta;
while(p){
//第一个循环用来把链表数据存入栈中
sta.push(p->data);
p = p->next;
}
p = head->next;
while(p){
//第二个循环时为了对比链表数据与栈弹出的数据
if(p->data != sta.top())
return false;
sta.pop();
p = p->next;
}
return true;
}
栈+快慢指针
空间复杂度:O(N/2)
bool isPalindrome2(Node* head){
if(head == NULL || head->next == NULL)
return true;
Node* right = head->next;
Node* cur = head;
while(cur->next != NULL && cur->next->next != NULL){
//cout << "right->data:" << right->data << endl;
//cout << "cur->data:" << cur->data << endl;
right = right->next;
cur = cur->next->next;
}
stack<Node*> sta;
while(right != NULL){
//把中点以后的部分放入栈中
sta.push(right);
//cout << "放入栈中的数据为:" << right->data << endl;
//cout << sta.top()->data << endl;
right = right->next;
}
while(!sta.empty()){
//cout << sta.top()->data << endl;
if(head->next->data != sta.top()->data){
return false;
}
head = head->next;
sta.pop();
}
return true;
}
有限个变量
bool isPalindrome3(Node* head){
if(head == NULL || head->next == NULL)
return true;
Node* n1 = head;
Node* n2 = head;
//find the mid
while(n2->next != NULL && n2->next->next != NULL){
//查找中间结点
cout << "n1->data : " << n1->data << endl;
cout << "n2->data : " << n2->data << endl;
n1 = n1->next;//n1指向中部
n2 = n2->next->next;//n2指向尾部
}
n2 = n1->next;//n2 right part first node
n1->next = NULL;
Node* n3 = NULL;
while(n2 != NULL){
//右半区反转
n3 = n2->next;//n3 save next node
n2->next = n1;//下一个反转结点
n1 = n2;//n1 move
n2 = n3;//n2 move
}
n3 = n1;//n3 save the last node
n2 = head->next;//left first node
bool res = true;
while(n1 != NULL && n2 != NULL){
if(n1->data != n2->data){
res = false;
break;
}
n1 = n1->next;//left to mid
n2 = n2->next;//right to mid
}
n1 = n3->next;
n3->next = NULL;
while(n1 != NULL){
//恢复列表
n2 = n1->next;
n1->next = n3;
n3 = n1;
n1 = n2;
}
return res;
}
2.5 将单向链表按某值划分成左边小,右边大的形式
左大右小怎么改
【题目】给定一个单链表的头结点head,结点的值类型为整型,再给定一个整数pivot。实现一个调整链表的函数,将链表调整为左部分是值小于pivot的结点,中间是对于pivot的结点,右边是大于pivot的结点
【进阶】【要求】调整后所有大于等于小于pivot的结点之间的相对顺序和调整之前一样。时间复杂度O(N),空间复杂度O(1)
【解题思路】
- 把链表的结点都放入到数组中,再对数组进行处理
- 利用六个指针,小于pivot的头指针和尾指针,等于pivot的头指针和尾指针,大于pivot的头指针和尾指针。依次对链表进行比较和分配,最后小于区域的尾指针连接等于区域的头指针;等于区域的尾指针连接大于区域的头指针。注意边界条件,可能没有大于pivot的数,或者没有等于、小于pivot的数
//对数组分区域
Node* listPartition(Node* head,const int pivot){
Node* sH = NULL;
Node* sT = NULL;
Node* eH = NULL;
Node* eT = NULL;
Node* mH = NULL;
Node* mT = NULL;
Node* next = NULL;//来保存下一个节点
while(head != NULL){
next = head->next;//保存断开结点的下一个结点
head->next = NULL;//把结点断开
//测试
//cout << "head->data: " << head->data << endl;
if(head->data < pivot){
//虽然以head开头,但是并不影响,如果先大后小,会不会有影响
if(sH == NULL){
sH = head;
sT = head;
}else{
sT->next = head;
sT = head;
}
}else if(head->data == pivot){
if(eH == NULL){
eH = head;
eT = head;
}else{
eT->next = head;
eT = head;
}
}else {
if(mH == NULL){
mH = head;
mT = head;
}else{
mT->next = head;
mT = head;
}
}
head = next;
}
//small and equal reconnect
if(sT != NULL){
//如果有小于区域
sT->next = eH;
eT = ((eT == NULL) ? sT : eT);
cout << "ST不为空" << endl;
/*
下一步,谁去连大于区域的头谁就变成eT,
就是说看有没有等于区域,如果没有就让sT去连,有就让eT连
*/
}
if(eT != NULL){
cout << "et不为空" << endl;
eT->next = mH;
}
return sH != NULL ? sH : (eH != NULL ? eH : mH);
}
2.6 复制含有随机指针结点的链表
【题目】一种特殊的单链表结点类描述如下
class Node{
public:
int data;
Node* next;
Node* rand;
Node(int value){
data = value;
}
};
【要求】rand指针是单链表结点结构中新增的指针,rand可能指向链表中的任意一个结点,也可以指向NULL,给定一个由Node结点类型组成的无环单链表的头结点head,实现一个函数完成单链表的复制,并返回复制的新链表的头结点。
【解题思路】
- 不考虑空间复杂度:用一个map : key(老链表结点) value(新链表结点),通过老结点的指针指向再确定新的指向
不考虑空间复杂度:
Node* copyList(Node* head){
hashMap<Node*,Node*> map;
Node* cur = head;
while(cur != NULL){
//把新老结点放入map中
map.put(cur,new Node*(cur->data))
cur = cur->next;
}
cur = head;
while(cur != NULL){
//cur:老
//map.get(cur) 新
map.get(cur).next = map.get(cur.next);
map.get(cur).rand = map.get(cur.rand);
cur = cur.next;
}
return map.get(head);
}
空间复杂度O(1)
Node* copyList2(Node* head){
if(head == NULL)
return NULL;
Node* cur = head;
Node* next = NULL;
//复制结点并把它连到被复制结点的后面
//1->1'->2->2'-3->3' .....
while(cur != NULL){
next = cur->next;
cur->next = new Node*(cur->data);
cur->next->next = next;
cur = next;
}
cur = head;
Node* curCopy = NULL;
//处理结点的rand指向
while(cue != NULL){
next = cur->next->next;
curCopy = cur->next;
curCopy->rand = cur->rand != NULL ? cur->rand->next : NULL;//注意这个next
cur = next;
}
//分离新老链表
Node* res = head->next;//老结点的头结点
cur = head;
while(cur != NULL){
next = cur->next->next;
curCopy = cur->next;
cur->next = next;
curCopy->next = next != NULL ? next.next : NULL;
cur = next;
}
return res;
}
2.7 两个链表相交问题
【题目】给定两个单链表,这两个单链表可能有环,也可能无环。其中头结点分别为head1、head2。实现一个函数,如果两个链表相交,请返回相交的第一个结点;如果不相交返回NULL;
【要求】如果两个链表长度之和为N,时间复杂度要求为O(N),额外空间复杂度为O(1)
【解题思路】
- 第一步:判断两个链表是否有环,有环返回入环结点,无环返回NULL
- 方法一:额外数据结构:哈希表:利用集合,如果集合中不存在这个结点,就把这个结点放入集合,如果存在则返回此结点
- 方法二:利用快慢指针,单链表只有一个指针,如果链表有环,那么链表会走不出来
- 快指针走两步,慢指针走一步
- 若有环,则快指针和慢指针会在环内相遇 ,且转的圈数不会超过两圈
- 快慢指针相遇后,快指针回到开始,满指针留在原地,之后快慢指针每次都走一步,则快慢指针会在入环结点处再次相遇;(结论)
- 若无环,快指针会走到NULL;
- 第二步:分情况讨论两个链表相交清空
- 如果两个链表均无环,两个链表相交,那么从相交的部分开始都相等 ,结尾处也一定相等
- 遍历两个链表,记录头结点、尾结点以及链表长度,然后比较尾结点,如果尾结点不相等,那么两个链表一定不相交。若相等,让长链表先走 两个链表长度之差 步。再让短链表与长链表一起走,两个链表结点相等时即为相交的第一个结点
- 如果两个链表均无环,两个链表相交,那么从相交的部分开始都相等 ,结尾处也一定相等
- 判断链表是否有环,有环返回入环结点,无环返回NULL
Node* getLoopNode(Node* head){
if(head == NULL || head->next == NULL || head->next->next == NULL)
return NULL;
Node* n1 = head->next;//慢指针先走一步
Node* n2 = head->next->next;//快指针走两步
while(n1 != n2){
if(n2->next == NULL || n2->next->next == NULL){
return NULL;
}
n1 = n1->next;
n2 = n2->next;
}
n2 = head;//快指针重新回到头部开始走,快慢指针各走一步
while(n1 != n2){
n1 = n1->next;
n2 = n2->next;
}
return n1;
}
2 . 分情况讨论
情况一:两个链表无环情况
Node* noLoop(Node* head1,Node* head2){
if(head1 == NULL || head2 == NULL)
return NULL;
Node* cur1 = head1;
Node* cur2 = head2;
int n = 0;//两个链表长度的差值,一个加,一个减,最后即为差
while(cur1->next != NULL){
n++;
cur1 = cur1->next;
}
while(cur2->next != NULL){
n--;
cur2 = cur2->next;
}
if(cur1 != cur2)//判断两个无环链表的尾结点是否相同,若不同说明两个链表不相交
return NULL;
cur1 = n > 0 ? head1 : head2;//谁长谁的头为cur1
cur2 = cur1 == head1 ? head2 : head1;//谁短谁的头变为cur2
n = abs(n);//取绝对值
while(n != 0){
//让长链表先走n步
n--;
cur1 = cur1->next;
}
while(cur1 != cur2){
//同步走,相遇时停止
cur1 = cur1->next;
cur2 = cur2->next;
}
return cur1;
}
情况二:两个单链表一个有环,一个无环,则不可能相交
情况三:两个单链表都有环
- 两个环各自独立;
- 相交,入环结点相同;
- 相交,入环结点不同;
//loop1,loop2分别为链表1,2的入环结点
Node* bothLoop(Node* head1,Node* loop1,Node* head2,Node* loop2){
Node* cur1 = NULL;
Node* cur2 = NULL;
if(loop1 == loop2){
//相交且入环结点相同,这个过程相当于求两个无环单链表相交的过程,结束结点为入环结点
cur1 = head1;
cur2 = head2;
int n = 0;
while(cur1 != loop1){
//结束结点为入环结点
n++;
cur1 = cur1->next;
}
while(cur2 == loop2){
n--;
cur2 = cur2->next;
}
if(cur1 != cur2)
return NULL;
cur1 = n > 0 ? head1 : head2;//谁长谁的头为cur1
cur2 = cur1 == head1 ? head2 : head1;//谁短谁的头变为cur2
n = abs(n);//取绝对值
while(n != 0){
//让长链表先走n步
n--;
cur1 = cur1->next;
}
while(cur1 != cur2){
//同步走,相遇时停止
cur1 = cur1->next;
cur2 = cur2->next;
}
return cur1;
}else{
//入环结点不同
cur1 = loop1->next;
while(cur1 != loop1){
//在环内遍历
if(cur1 == loop2)//环内有点等于loop2 说明两个环相交,不等于说明两个环独立
return loop1;
cur1 = cur1.next;
}
return NULL;
}
}
//主函数调用
Node* getIntersectNode(Node* head1,Node* head2){
if(head1 == NULL || head2 == NULL)
return NULL;
//得到链表的入环结点,没有环返回NULL
Node* loop1 = getLoopNode(head1);
Node* loop2 = getLoopNode(head2);
//判断是否有环并处理
//两个单链表均无环
if(loop1 == NULL && loop2 == NULL){
return noLoop(head1,head2);
}
//一个有环一个无环则不可能相交
//两个链表均有环
if(loop1 != NULL && loop2 != NULL){
return bothLoop(head1,loop1,head2,loop2);
}
return NULL;
}