链表
优点:能灵活的分配内存空间,能在O(1)时间内删除或添加元素
缺点:查询元素需要O(n)时间
适用场景:数据元素个数不确定,经常进行数据的添加或删除
在leetcode中默认的链表结构是
struct ListNode {
int val;
ListNode* next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode* next) : val(x), next(next) {}
};
在链表中比较常见的解题方法有:
1.对于求链表长度,或者判断链表是否有环等问题,可以使用快慢指针进行结局
2.对于需要插入或者删除的操作,建一个哑(哨兵)指针指向head节点会使对所有链表的操作统一起来,并且通常需要创建一个临时指针来记录前面被指向的节点
3.对一些依赖于后面节点才可以完成的操作,可以使用递归的方式来解决
1.链表反转
使用迭代法的思路需要构建三个指针,分别是当前节点的前驱pre,当前节点cur,和当前节点的后继next
在循环遍历的过程中,然后让cur指向pre ,再把cur和pre的位置往后移一位即可
ListNode* pre = nullptr;//刚开始当前节点的上一个为空
ListNode* cur = head; //当前节点指向head
while (cur) {
ListNode* next = cur->next; //当前节点的下一个
cur->next = pre; //把当前节点指向当前节点上一个
pre = cur; //向后移动
cur = next;
}
return pre;
时间复杂度为O(n),空间复杂度为O(1);
使用递归法的思路是一直让链表递归到最后一个节点记为newHead然后返回
在返回过程中,让当前节点的下一个指向当前节点
并且把当前节点指向空
等到递归函数全部出栈后,递归完成
ListNode* reverseList(ListNode* head) {
while (!head || !head->next) {
return head;
}
ListNode* newHead = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return newHead;
}
时间复杂度为O(n),空间复杂度为O(n)
2.合并有序链表
给定两个增序的链表,合成一个增序的链表
递归法:
ListNode* mergeTwoLists(ListNode* l1,ListNode* l2) {
if (!l1) return l2;
if (!l2) return l1;
if (l1->val > l2->val) {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
else {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}
}
时间复杂度为O(n),空间复杂度为O(n)
迭代法:
ListNode* mergeTwoLists(ListNode* l1,ListNode* l2) {
ListNode* dummy = new ListNode(0);//创建一个头节点
ListNode* cur = dummy;
while (l1 && l2) {
if (l1->val > l2->val) {
cur->next = l2;
l2 = l2->next;
}
else {
cur->next = l1;
l1 = l1->next;
}
cur = cur->next;
}
cur->next = l1 ? l1 : l2;//循环结束的时候可能l1或l2有一个并不为空,所以需要把后续的部分也接上
return dummy->next;
}
时间复杂度为O(n),空间复杂度为O(1)
3.交换相邻节点
给定一个链表,交换每个相邻的一对节点
迭代法:
首先创建一个虚拟节点,让虚拟节点dummy指向head
让node指向node2
node1指向node2的next
让node2指向node1
也就是这样
最后把node移动到node1的位置即可
ListNode* swapPairs(ListNode* head) {
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* node = dummy;
while (node->next != nullptr && node->next->next != nullptr) {
ListNode* node1 = node->next;
ListNode* node2 = node->next->next;
node->next = node2;
node1->next = node2->next;
node2->next = node1;
node = node1;
}
return dummy->next;
}
时间复杂度是O(n),空间复杂度是O(1)
递归法:
递归的终止条件是链表中没有节点或者只有一个节点,在这种情况下就不必进行交换了。首先设置head节点的next为newHead,让head->next去继续递归,让newHead的next指向head即可
ListNode* swapPairs(ListNode* head) {
while (head == nullptr || head->next == nullptr) {
return head;
}
ListNode* newHead = head->next;
head->next = swapPairs(newHead->next);
newHead->next = head;
return newHead;
}
时间复杂度是O(n),空间复杂度是O(n)
4.链表相交
给定两个链表,判断它们是否相交于一点,并求这个相交节点。
思路:对于两个链表A,长度为m,链表B,长度为n来说
如果链表相交的话,并且m=n的话,那么遍历A和B的过程中会同时到达A和B相交的节点,此时A=B,返回该节点即可
如果链表相交,并且m!=n的话,那么pA指针先走一遍A再走一边B,同时Pb指针先走一边B再走一遍A,在过程中两个指针会指向他们的相交的节点,此时返回该节点即可。
如果链表不相交的话并且m=n的话,那么两个指针同时到达两个链表的尾节点,此时返回空
如果链表不相交且m!=n的话,pA会在移动m+n次,pB会在移动n+m次以后分别到达链表B和链表A的尾节点,此时返回空
ps:错的人就算走对方的路也还是会错
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
if (headA == nullptr && headB == nullptr) {
return nullptr;
}
ListNode* pA = headA, * pB = headB;
while (pA != pB) {
pA = pA == nullptr ? headB : pA->next;
pB = pB == nullptr ? headA : pB->next;
}
return pA;
}
时间复杂度O(m+n),空间复杂度O(1)
5.删除排序链表重复元素
解法:先定义一个node指针指向head,遍历链表,如果两个节点相同,那么让node的next指向node的next的next,否则,node往后继续走
ListNode* deleteDuplicates(ListNode* head) {
ListNode* node = head;
while (node != nullptr) {
if (node->next != nullptr && node->next->val == node->val) {
node->next = node->next->next;
}
else {
node = node->next;
}
}
return head;
}
时间复杂度O(n),空间复杂度O(1)
6.奇偶链表
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性
ListNode* oddEvenList(ListNode* head) {
if (head == nullptr || head->next == nullptr) {//head为空或者链表只有一个结点的话直接返回
return head;
}
ListNode* p1 = head;
ListNode* p2 = head->next;
ListNode* head1 = head->next;//用来保存偶数节点的开头
while (p2 && p2->next) {
p1->next = p2->next;
p1 = p1->next;
p2->next = p1->next;
p2 = p2->next;
}
p1->next = head1;//把奇数节点的结尾接到偶数节点的开头上
return head;
}
时间复杂度O(n),空间复杂度O(1)
7.删除链表第n个节点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点
思路:使用快慢指针,首先让fast指针先走n步,然后和slow指针同时走,直到fast指针走到尾节点位置,此时slow指针就会在要被删除的位置上。但是如果要删除指针的话还是最好能使slow节点移动到要被删除指针的前驱节点删除操作,所以本解法使用了哑指针dummy,这样做的好处一是可以移动n的时候直接移动到被删除节点的前驱节点,好处二是在链表只有一个节点,并要删除一个节点的情况下,可以使头节点能有一个哑节点作为前驱节点,从而方便删除操作
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0);//创建哑节点
dummy->next = head;//哑节点作为head的前驱节点
ListNode* fast = dummy;
ListNode* slow = dummy;
while (n > 0) {
fast = fast->next;
n--;
}
while (fast->next != nullptr) {//slow就是要被删除节点的前驱节点
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;//删除操作
return dummy->next;
}
时间复杂度O(n),空间复杂度O(1)
8.回文链表
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false
解法:使用快慢指针确定链表的中点,然后反转链表的后半段,然后对头尾遍历比较
ListNode* mid(ListNode* head) { //快慢指针
ListNode* fast = head;
ListNode* slow = head;
while (fast->next != nullptr && fast->next->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
ListNode* reverse(ListNode* head) { //反转链表
ListNode* pre = nullptr;
ListNode* cur = head;
while (cur) {
ListNode* next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
return pre;
}
bool isSame(ListNode* l1, ListNode* l2) {
while(l2){ //可能存在链表长度为1的情况下l2为空
if(l1 -> val != l2 -> val){
return false;
}
l1 = l1->next;
l2 = l2->next;
}
return true;
}
bool isPalindrome(ListNode* head) {
if(head == nullptr || head->next == nullptr) return true;
ListNode* pMid = mid(head);
ListNode* endHead = reverse(pMid->next);
return isSame(head, endHead);
}
时间复杂度O(n),空间复杂度O(1)
9.合并K个有序链表
解法:可以使用之前实现的合并两个有序链表的函数, 逐一合并两条链表
ListNode* mergeKLists(vector<ListNode*>& lists) {
int n = lists.size();
ListNode* dummy = nullptr;
for (int i = 0; i < n; ++i) {
dummy = mergeTwoLists(dummy, lists[i]);
}
return dummy;
}
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode* dummy = new ListNode(0);//创建一个头节点
ListNode* cur = dummy;
while (l1 && l2) {
if (l1->val > l2->val) {
cur->next = l2;
l2 = l2->next;
}
else {
cur->next = l1;
l1 = l1->next;
}
cur = cur->next;
}
cur->next = l1 ? l1 : l2;//循环结束的时候可能l1或l2有一个并不为空,所以需要把后续的部分也接上
return dummy->next;
}
时间复杂度:假设链表最长的长度是n,第一次合并的时间复杂度是n,第二次合并的时间复杂度是2n,所以第K次合并的时间复杂度是kn,根据等差数列求和公式n(a1+an)/2,K次合并的总时间复杂度为O(k(n+kn)/2) = O(k²n)
空间复杂度:没有使用任何辅助空间,所以空间复杂度是O(1)
10.判断链表是否成环
解法:快慢指针,如果有环则快指针会追上慢指针,如果没有环,则快指针会指向空
bool hasCycle(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return false;
}
ListNode* fast = head->next;
ListNode* slow = head;
while (fast != slow) {
if (fast == nullptr || fast->next == nullptr) {
return false;
}
fast = fast->next->next;
slow = slow->next;
}
return (fast == nullptr || fast->next == nullptr) ? false : true;
}
时间复杂度O(n),空间复杂度O(1)