链表初步学习记录(自用)

途径:链表学习

一、单链表

  • 单链表中的每个结点不仅包含值,还包含链接到下一个结点的引用字段。通过这种方式,单链表将所有结点按顺序组织起来。

单链表例子

蓝色箭头显示单个链接列表中的结点是如何组合在一起的。

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)将 curnext 字段链接到 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.链接 curprevnext,其中 nextprev 原始的下一个节点;

前一个记为p,后一个记为n。

cur->prev=p;

cur->next=n;

2.用 cur 重新链接 prevnext

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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值