链表的经典技巧及算法

1 寻找链表的中间节点:最简单的方法是,先遍历一遍链表,计算出链表的长度,然后计算出中间节点的位置,然后再遍历一遍,边遍历边计算,直到找到中间节点,这个方法略显啰嗦,最坏的情况需要遍历2次链表,代码如下:

ListNode* findMiddle(ListNode* head){

    if(!head) return NULL;  

    int length = 0;
    while(head) { head = head->next; length ++ ; }

    int middle = (lenght+1)/2;
    --middle;
    while(middle--) head = head->next;

    return head;
}

另一个更灵巧的方法是,用两个指针,慢指针每次走一步,快指针每次走两步,当快指针走到链表的末端(NULL)时,慢指针正好指向了中间节点,代码如下:

ListNode* findMiddle(ListNode* head){

    if(!head) return NULL;  

    ListNode* slow = head;
    ListNode* fast = head->next;

    while(fast && fast->next){
        slow = slow->next;
        fast = fast->next->next;
    }

    return slow;
}

2 检测链表是否有环:经典的做法也是用快慢指针,如果没有环,快指针一定先到达链表的末端(NULL),如果有环,快、慢指针一定会相遇在环中,代码如下:

bool hasCircle(ListNode* head, ListNode **node){

    if(!head) {
  *node = NULL; return false; }

    ListNode* slow = head;
    ListNode* fast = head->next;

    while(fast && fast->next && fast!=slow){
        slow = slow->next;
        fast = fast->next->next;
    }

    if(fast==slow) {
  *node = fast; return true; }
    else {
  *node = NULL; return false; }
}

3 检测环的入口:经典的做法是先检测是否有环,如果有环,则计算出环的长度,然后使用前后指针(不是快慢指针),所谓的前后指针就是一个指针先出发,走了若干步以后,第二个指针也出发,然后两个指针一起走,当前后指针相遇时,它们正好指向了环的入口,代码如下:

ListNode* detectEntry(ListNode* head){

    //判断是否有环,无环则返回NULL
    ListNode* node = NULL;
    if(!hasCircle(head, &node)) return NULL;

    //计算环的长度
    int circleLen = 1; ListNode *temp = node->next;
    while(temp!=node) { temp = temp->next; circleLen ++; }

    //使用前后指针,此时fast每次也只移动一步
    ListNode *first = head , *second = head;
    while(circleLen--) first = first->next; // first指针先出发

    //然后两个指针同时向前走,每次走一步
    while(first!=second){ first = first->next; second = second->next ; }

    return first;
}

如果允许使用额外的内存,可以有更简单的做法,即一边遍历,一边将节点放在map中,当某个节点第二次出现在map中时,它就是入口节点,代码如下:

ListNode* detectEntry(ListNode* head){

    if(!head) return NULL;

    map<ListNode*,bool> hm;

    while(!head){
        if(hm.count(head)) return head;
        else hm[head] = true;

        head = head->next;
    }

    return NULL;
}

4 链表翻转:假设原链表为1->2->3,翻转以后的链表应该是1<-2<-3,即节点3变成了头节点,代码如下:

ListNode* reverseList(ListNode *head){
    if(!head || !head->next) return head;

    ListNode* p = head->next;
    head->next = NULL;

    while(p){
        ListNode* t = p->next;
        p->next = head;
        head = p;
        p = t;
    }

    return head;
}

5 删除链表中的节点,注意这里只给出要删除的那个节点,不给出链表头(假设被删除节点不是尾节点),代码如下:

void removeNode(ListNode *target){

    if(!target) return ;

    ListNode *t = target->next;
    target->val = t->val;
    target->next = t->next;  //用target的下一个节点覆盖了target,然后删除下一个节点即可

    delete t;

}

如果被删除节点是尾节点,上面的代码就无法将target上一个节点的next置为NULL,所以只有给了头节点后,才能遍历到target的上一个节点,并把其next置为NULL。

6 回文链表的检测:所谓回文链表,即链表元素关于中间对称,,如1->2->3->2->1,比较简单的方法是用一个栈,先顺序遍历链表,并把每个节点放入栈中,遍历完成后,栈的出栈顺序正好是原链表的逆,代码如下:

bool isPalindrome(ListNode* head){
    if(!head) return true; //空链表姑且认为是回文的

    stack<ListNode*> s;
    ListNode* p = head;
    while(p) { s.push(p); p = p->next ; }

    int length = s.size();
    length /= 2;

    while(length--) {
        p = s.top();
        s.pop();

        if(p->val!=head->val) return false;
        head = head->next;
    }

    return true;    
}

上面代码的空间复杂度为O(N),其实还有空间复杂度为O(1)的算法,也很灵巧,运用了之前提到的一些技巧,代码如下:

bool isPalindrome(ListNode* head) {
    if (!head || !head->next) return true; //空链表或只有一个元素的链表都认为是回文的

    //寻找链表的中间节点
    ListNode* slow = head;
    ListNode* fast = head->next;

    while (fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
    }

    //反转后半段链表
    ListNode* prev = slow; //prev目前是前半段链表的尾节点,后半段链表的头结点
    ListNode* p = slow->next;
    slow->next = nullptr; 

    while (p)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值