途径:链表学习
一、单链表
- 单链表中的每个结点不仅包含值,还包含链接到下一个结点的引用字段。通过这种方式,单链表将所有结点按顺序组织起来。
蓝色箭头显示单个链接列表中的结点是如何组合在一起的。
1.结点结构
单链表中结点的典型定义:
// Definition for singly-linked list.
struct SinglyListNode {
int val;
SinglyListNode *next;
SinglyListNode(int x) : val(x), next(NULL) {}
};
// Definition for singly-linked list.
public class SinglyListNode {
int val;
SinglyListNode next;
SinglyListNode(int x) { val = x; }
}
在大多数情况下,我们将使用头结点(第一个结点)来表示整个列表。
2.操作
与数组不同,我们无法在常量时间内访问单链表中的随机元素。 如果我们想要获得第 i 个元素,我们必须从头结点逐个遍历。 我们按索引来访问元素平均要花费 O(N) 时间,其中 N 是链表的长度。
例如,在上面的示例中,头结点是 23。访问第 3 个结点的唯一方法是使用头结点中的“next”字段到达第 2 个结点(结点 6); 然后使用结点 6 的“next”字段,我们能够访问第 3 个结点。
1.如果想要在给定的结点prev后面添加新值,我们应该:
(1)使用给定值初始化新结点 cur
;
(2)将 cur
的 next
字段链接到 prev
的下一个结点 next
;
(3)将 prev
中的 next
字段链接到 cur
。
与数组不同,我们不需要将所有元素移动到插入元素之后。因此,您可以在
O(1)
时间复杂度中将新结点插入到链表中,这非常高效。
2.在开头添加结点
一般用头结点来代表整个列表
(1)初始化一个新结点cur
;
(2)将新结点链接到原来的头结点head
;
(3)将cur指定为head
。
3.在结尾类似
-
删除操作
1.删除结点cur
(1)找到 cur 的上一个结点
prev
及其下一个结点next
;(2)接下来链接
prev
到 cur 的下一个节点next
。删除结点的时间复杂度将是
O(N)
,空间复杂度为O(1)
。2.删除第一个结点
可以简单地
将下一个结点分配给 head
。
实践:设计链表
typedef struct MyLinkedList{
int val;
struct MyLinkedList* next;
} MyLinkedList;
MyLinkedList* myLinkedListCreate() {
MyLinkedList *node=(MyLinkedList*)malloc(sizeof(MyLinkedList));
node->next=NULL;
return node;
}
int myLinkedListGet(MyLinkedList* obj, int index) {
if(index<0){
return -1;
}
MyLinkedList* p=obj;
int i;
for(i=0;i<=index;i++){
if(p->next==NULL){
return -1;
}else{
p=p->next;
}
}
return p->val;
}
void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
MyLinkedList *head=(MyLinkedList*)malloc(sizeof(MyLinkedList));
head->val=val;
head->next=obj->next;//obj->next才是头指针
obj->next=head;
}
void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
MyLinkedList *tail=(MyLinkedList*)malloc(sizeof(MyLinkedList)),*p=obj;
tail->val=val;
tail->next=NULL;
while(p->next){
p=p->next;
}
p->next=tail;
}
void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
if(index<0){
myLinkedListAddAtHead(obj,val);
}
int i;
MyLinkedList *p=obj;
for(i=0;i<index;i++){
if(p->next==NULL){
return;
}else{
p=p->next;
}
}
MyLinkedList *mid=(MyLinkedList*)malloc(sizeof(MyLinkedList));
mid->val=val;
mid->next=p->next;
p->next=mid;
}
void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
if(index<0){
return ;
}
MyLinkedList* p=obj,*q;
int i;
for(i=0;i<index;i++){
if(p->next==NULL){
return ;
}else{
p=p->next;
}
}
if(p->next==NULL)return ;
q=p->next;
p->next=q->next;
}
void myLinkedListFree(MyLinkedList* obj) {
MyLinkedList *p1,*p2;
for(p1=obj,p2=p1->next;p2;p2=p2->next){
free(p1);
p1=p2;
}
}
二、双指针技巧
经典问题:给定一个链表,判断链表中是否有环。
使用两个速度不同的指针:
-
如果没有环,快指针将停在链表的末尾。
-
如果有环,快指针最终将与慢指针相遇。
一个安全的选择是每次移动慢指针一步,而移动快指针两步。每一次迭代,快速指针将额外移动一步。如果环的长度为 M,经过 M 次迭代后,快指针肯定会多绕环一周,并赶上慢指针。
实践1:环形链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
bool hasCycle(struct ListNode *head) {
if(head==NULL)return false;
struct ListNode *p1=head,*p2=head;
while(p2!=NULL&&p2->next!=NULL){
p1=p1->next;
p2=p2->next;
if(p2==NULL){
return false;
}else{
p2=p2->next;
}
if(p2==p1){
return true;
}
if(p2==NULL){
return false;
}
}
return false;
}
实践2:环形链表2
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *slow=head,*fast=head;
while(fast!=NULL&&fast->next!=NULL){
slow=slow->next;
fast=fast->next;
fast=fast->next;
if(fast==slow){
break;
}
}
if(fast==NULL||fast->next==NULL){
return NULL;
}
slow=head;
while(slow!=fast){
slow=slow->next;
fast=fast->next;
}
return slow;
}
实践3:相交链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
struct ListNode *a=headA,*b=headB;
int ret=0;
for(;a;a=a->next){
for(b=headB;b;b=b->next){
if(b==a){
ret=1;
break;
}
}
if(ret==1)break;
}
if(ret==0){
return NULL;
}else{
return a;
}
}
实践4:删除链表的倒数第n个节点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeNthFromEnd(struct ListNode* head, int n){
struct ListNode *fast,*slow;
fast=head;
slow=head;
int i;
for(i=0;i<n;i++){
fast=fast->next;
}
if(fast==NULL)return head->next;
while(fast!=NULL&&fast->next!=NULL){
fast=fast->next;
slow=slow->next;
}
slow->next=(slow->next)->next;
return head;
}
小结:
- 注意1: 在调用 next 字段之前,始终检查节点是否为空。
- 注意2:仔细定义循环的结束条件。
- 复杂度:空间复杂度:O(1);时间复杂度需要分析循环次数(快指针比慢指针每次多走1,则为 O(1)。
三、经典问题
反转链表
其中一种方法:按原始顺序迭代结点
,并将它们逐个移动到列表的头部
。
时间复杂度O(N),空间复杂度O(1)。
实践1:反转链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head){
struct ListNode *p,*q=head;
if(head!=NULL&&head->next!=NULL){
p=head->next;
head->next=p->next;
p->next=q;
q=p;
p=head->next;
while(head->next!=NULL){
head->next=p->next;
p->next=q;
q=p;
p=head->next;
}
}else{
return head;
}
return q;
}
实践2:移除链表元素
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode*p=head,*q=NULL;
while(p){
if(p->val==val){
if(q){
q->next=p->next;
free(p);
p=q->next;
}else{
head=head->next;
free(p);
p=head;
}
}else{
q=p;
p=p->next;
}
}
return head;
}
实践3:奇偶链表
struct ListNode* oddEvenList(struct ListNode* head){
struct ListNode *head1=NULL,*p=head,*q=NULL,*a=NULL;
int i=1;
while(p!=NULL){
if(i%2==0){
if(head1){
a->next=p;
q->next=p->next;
p->next=NULL;
p=q->next;
a=a->next;
}else{
head1=p;
q->next=p->next;
head1->next=NULL;
p=q->next;
a=head1;
}
}else{
q=p;
p=p->next;
}
i++;
}
if(head1){
q->next=head1;
}
return head;
}
实践4:回文链表
bool isPalindrome(struct ListNode* head){
struct ListNode *p=head;
int ret=1,a[100000],i=0,n;
while(p){
a[i++]=p->val;
p=p->next;
}
n=i;
for(i=0;i<n;i++){
if(a[i]!=a[n-i-1]){
ret=0;
break;
}
}
return ret==1;
}
小结:
1.通过一些测试用例可以节省时间: 使用链表时不易调试。因此,在编写代码之前,自己尝试几个不同的示例来验证自己算法总是很有用的。
**2.可以同时使用多个指针:**当为链表问题设计算法时,可能需要同时跟踪多个结点。应该记住需要跟踪哪些结点,并且可以自由地使用几个不同的结点指针来同时跟踪这些结点。如果使用多个指针,最好为它们指定适当的名称,以防将来必须调试或检查代码。
**3. 在许多情况下,你需要跟踪当前结点的前一个结点:**由于单链表中的前一个结点无法追溯,因此,不仅要存储当前结点,还要存储前一个结点。(这在双链表中是不同的)
四、双链表
1.简介
双链表多一个引用字段,称为
“prev”`字段。有了这个额外的字段,能够知道当前结点的前一个结点。
与单链表类似,我们将介绍在双链表中如何访问数据、插入新结点或删除现有结点。
我们可以与单链表相同的方式访问数据:
-
我们不能在常量级的时间内**访问随机位置。
-
我们必须从头部遍历才能得到我们想要的第一个结点。
-
在最坏的情况下,时间复杂度将是 O(N),其中 N 是链表的长度。
对于添加和删除,会稍微复杂一些,因为我们还需要处理“prev”字段。
2.添加操作
想在现有的结点 prev
之后插入一个新的结点 cur
1.链接 cur
与 prev
和 next
,其中 next
是 prev
原始的下一个节点;
前一个记为p,后一个记为n。
cur->prev=p;
cur->next=n;
2.用 cur
重新链接 prev
和 next
。
p->next=cur;
q->prev=cur;
与单链表类似,添加操作的时间和空间复杂度都是 O(1)
。
开头
或结尾
添加类似。
3.删除操作
如果我们想从双链表中删除一个现有的结点 cur,我们可以简单地将它的前一个结点 prev 与下一个结点 next 链接起来。
与单链表不同,使用“prev”字段可以很容易地在常量时间内获得前一个结点。
不再需要遍历链表来获取前一个结点,时间和空间复杂度都是O(1)。
实践:设计链表
typedef struct MyLinkedList{
int val;
struct MyLinkedList *next,*prev;
} MyLinkedList;
MyLinkedList* myLinkedListCreate() {
MyLinkedList *p=(MyLinkedList*)malloc(sizeof(MyLinkedList));
p->next=NULL;
p->prev=NULL;
p->val=0;//用于记录链表结点数
return p;
}
int myLinkedListGet(MyLinkedList* obj, int index) {
if(index<0||index>=obj->val){
return -1;
}
MyLinkedList *p=obj->next;
int i;
for(i=0;i<index;i++){
p=p->next;
}
if(p)
return p->val;
else
return -1;
}
void myLinkedListAddAtHead(MyLinkedList* head, int val) {
MyLinkedList *cur=myLinkedListCreate();
cur->val=val;
if(head->next){
cur->next=head->next;
cur->prev=head;
head->next->prev=cur;
head->next=cur;
}else{
cur->prev=head;
head->next=cur;
cur->next=NULL;
}
head->val++;
}
void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
MyLinkedList *cur=myLinkedListCreate(),*p=obj;
cur->val=val;
int i;
for(i=obj->val;i>0;i--){
p=p->next;
}
cur->prev=p;
p->next=cur;
obj->val++;
}
void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
if(index<=0){
myLinkedListAddAtHead(obj,val);
}else if(index>obj->val){
return ;
}else if(index==obj->val){
myLinkedListAddAtTail(obj,val);
}else{
MyLinkedList *cur=myLinkedListCreate(),*p=obj;
int i;
cur->val=val;
for(i=index;i>0;i--){
p=p->next;
}
p->next->prev=cur;
cur->next=p->next;
cur->prev=p;
p->next=cur;
obj->val++;
}
}
void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
MyLinkedList *p=obj->next;
if(index<0||index>=obj->val){
return ;
}
if(index==0){
if(p&&p->next){
p->next->prev=obj;
obj->next=p->next;
}else{
obj->next=NULL;
}
obj->val--;
}else{
int i;
for(i=index;i>0;i--){
p=p->next;
}
if(p->next){
p->next->prev=p->prev;
p->prev->next=p->next;
}else{
p->prev->next=NULL;
}
obj->val--;
}
}
/**
* Your MyLinkedList struct will be instantiated and called as such:
* MyLinkedList* obj = myLinkedListCreate();
* int param_1 = myLinkedListGet(obj, index);
* myLinkedListAddAtHead(obj, val);
* myLinkedListAddAtTail(obj, val);
* myLinkedListAddAtIndex(obj, index, val);
* myLinkedListDeleteAtIndex(obj, index);
* myLinkedListFree(obj);
*/
五、小结
1.单链表和双链表在许多操作中是相似的:
- 它们都无法在常量时间内随机访问数据。、
- 它们都能够在 O(1) 时间内在给定结点之后或列表开头添加一个新结点。
- 它们都能够在 O(1) 时间内删除第一个结点。
但是删除给定结点(包括最后一个结点)时略有不同。
- 在单链表中,它无法获取给定结点的前一个结点,因此在删除给定结点之前我们必须花费 O(N) 时间来找出前一结点。
- 在双链表中,这会更容易,因为我们可以使用“prev”引用字段获取前一个结点。因此我们可以在 O(1) 时间内删除给定结点。
- 需要经常添加/删除结点,链表更好。
- 需要经常按索引访问元素,数组更好。
实践:合并两个有序链表
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){//递归
//struct ListNode *p1=l1,*q1=NULL,*p2=l2,*q2=NULL;
if(l1==NULL){
return l2;
}else if(l2==NULL){
return l1;
}
if(l1->val < l2->val){
l1->next=mergeTwoLists(l1->next,l2);
return l1;
}else{
l2->next=mergeTwoLists(l1,l2->next);
return l2;
}
}
实践:两数相加
struct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2){
struct ListNode *head=l1,*p1=l1,*p2=l2,*cur=(struct ListNode *)malloc(sizeof(struct ListNode));
int sum=0;
if(!p1->next&&!p2->next){
sum=p1->val+p2->val;
if(sum>9){
p1->val=sum%10;
cur->next=NULL;
cur->val=1;
p1->next=cur;
}else{
p1->val=sum;
}
return head;
}
while(p1->next&&p2->next){
sum=p1->val+p2->val;
if(sum>9){
p1->next->val++;
}
p1->val=sum%10;
p1=p1->next;
p2=p2->next;
}
if(!p1->next&&!p2->next){
sum=p1->val+p2->val;
if(sum>9){
p1->val=sum%10;
cur->next=NULL;
cur->val=1;
p1->next=cur;
}else{
p1->val=sum;
}
return head;
}
if(p1->next){
sum=p1->val+p2->val;
if(sum<=9){
p1->val=sum;
return head;
}else{
p1->val=sum%10;
p1->next->val++;
p1=p1->next;
while(p1){
if(p1->val>9){
p1->val%=10;
if(p1->next){
p1->next->val++;
}else{
cur->val=1;
cur->next=NULL;
p1->next=cur;
return head;
}
}else{
return head;
}
p1=p1->next;
}
}
}else{
sum=p1->val+p2->val;
if(sum<=9){
p1->val=sum;
p1->next=p2->next;
return head;
}else{
p1->val=sum%10;
p1->next=p2->next;
p1->next->val++;
p1=p1->next;
while(p1){
if(p1->val>9){
p1->val%=10;
if(p1->next){
p1->next->val++;
}else{
cur->val=1;
cur->next=NULL;
p1->next=cur;
return head;
}
}else{
return head;
}
p1=p1->next;
}
}
}
return head;
}
实践:旋转链表
struct ListNode* rotateRight(struct ListNode* head, int k){
struct ListNode *len=head,*p=head,*p1=head,*q=head;
if(!head){
return head;
}
int cnt=0;
while(len){
cnt++;
len=len->next;
}
if(k==0||cnt==1||k%cnt==0){
return head;
}
k%=cnt;
int n=cnt-k,i;
for(i=0;i<n-1;i++){
p=p->next;
p1=p1->next;
}
p1=p1->next;
for(i=0;i<cnt-1;i++){
q=q->next;
}
q->next=head;
p->next=NULL;
return p1;
}