考研算法(一) 线性表

1.二分法

模板:

bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用: //查找满足右边性质的最左元素
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用://查找满足左边性质的最右元素
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

使用二分的前提是具有两段性,左侧元素具有一个性质,右侧不具有这个性质。查找左侧的最右元素和右侧的最左元素常用二分。

实例:

    int l = 0, r = n-1;  //查找右边大于等于x的最小元素
    while(l < r){
        int mid = l + r >> 1;
        if(a[mid] >= x) r = mid;
        else l = mid+1;
    }
    if(a[r]!=x) printf("-1 -1\n");//查找失败

2.回文列表

请判断一个链表是否为回文链表。

思想:

将链表一分为二,难点在于结点个数是奇数还是偶数不确定,需要判断一下。如果是奇数就跳过中间结点,否则要考虑中间结点。前后两个链表的一致可以用栈来判断,也可以用数组,倒序遍历比较就行了。

        //     s-慢指针 f-快指针
        //     循环结束时的状态如下:
        //     1 2 2 1 NULL 偶数长度 后半部分起点就是s
        //           s         f
        // else
        //     1 2 3 2 1 奇数长度  后半部分起点是s的下一个
        //           s     f
        
    bool isPalindrome(ListNode* head) {
        if(!head||!head->next) return true;//0个或1个数自然为真
        stack<int> stk;//存放前一半链表
        auto f = head,s = head;
        while(f&&f->next){
            stk.push(s->val);
            s = s->next;
            f = f->next->next;
        }
        if(f) s = s->next;//后半部分起点
        while(s){
            if(s->val!=stk.top()) return false;
            stk.pop(),s = s->next;
        }
        return true;
    }

3.删除列表的倒数第n个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

思想

首先后指针移动到第n个结点,然后前后指针同时后移,直到后指针的指针指向空,此时前指针指向倒数第n+1个结点,删去后一个结点即可。

    ListNode* removeNthFromEnd(ListNode* head, int n) {
        auto *h=new ListNode(-1);//辅助头结点,避免换头的判断
        h->next=head;
        auto f=h,s=h;
        while(n--&&f->next) f=f->next;
        while(f->next) f=f->next,s=s->next;
        s->next=s->next->next;
        //if(s->next) s->next->last=s;  //如果为双链表的话多一步
        return h->next;
    }

4.合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

思想:

类似于归并排序的并阶段,将两个有序数组合并。在两个数组上各维持一个位置指针,比较大小,依次排列即可。

    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {//迭代版
        auto h = new ListNode(-1), r = h;//r为尾结点
        while(l1&&l2){
            if(l1->val < l2->val) r->next = l1,l1 = l1->next,r = r->next;//把l1连到尾部
            else r->next = l2,l2 = l2->next,r = r->next;
        }
        if(!l1) r->next = l2; 
        if(!l2) r->next = l1;
        return h->next;
    }
    
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {//递归版
        if(!l1) return l2;//空时
        if(!l2) return l1;
        if(l1->val < l2->val){
            l1->next = mergeTwoLists(l1->next, l2);
            return l1;
        } 
        else{
            l2->next = mergeTwoLists(l1,l2->next);
            return l2;
        } 
    }

5.旋转链表

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

思想:

首先统计链表结点的个数n,k对n取模。后指针移动到第k位,然后前后指针同时后移直到后指针的指针为空,此时前指针指向倒数第k+1个结点,将后指针的指针指向头结点,头指针指向前指针的后面一个结点,前指针的指针指向空。画图易懂。

注意:

不能在用到某个结点的指针前给这个结点的指针赋值。千万别犯这个错误。

   ListNode* rotateRight(ListNode* head, int k) {
        if(!head) return head;
        auto f = head, s = head;
        int n = 0;
        while(f){n++,f = f->next;}
        k = k%n;
        f = head;
        while(k-- && f->next) f = f->next;
        while(f->next) s = s->next, f = f->next;
        f->next = head;
        head = s->next;
        s->next = NULL;
        return head;
    }

6.反转链表

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

思想:

首先前指针指向新建的头结点,后指针指向原来的头结点。后指针后移n-m位到第n-m+1个结点,然后前后指针同时后移m-1位,后指针此时位于第n位,前指针位于第m-1位。然后执行以前指针的后一个结点为头,后指针为尾的翻转操作,执行完后将指针位置对应即可。

    ListNode* reverseBetween(ListNode* head, int m, int n) {
        if(m==n) return head;
        auto h = new ListNode(-1);
        h->next = head;
        auto a = h,d = h->next;
        for(int i = n-m;i > 0;i--) d = d->next;
        while(m-->1){
            a = a->next;
            d = d->next;
        }
        auto c = a->next, b = d->next;
        for(auto p = c, q = c->next; q!=b;){
            auto r = q->next;
            q->next = p;
            p = q;
            q = r;
        }
        a->next = d;
        c->next = b;
        return h->next;
    }

7.有序链表转换二叉搜索树

给定一个单链表,其中的元素按升序排序,转换为高度平衡的二叉搜索树。

本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。

思想:

万物皆可二分。无论树的结点是奇数个还是偶数个,最终的结果都是平衡的。这一建树思想广泛用于线段树。

    vector<int> v;
    int mid;
    TreeNode* sortedListToBST(ListNode* h) { 
        v.clear();
        while(h){
            v.push_back(h->val);
            h=h->next;
        }
       return buildTree(0,v.size()-1);  
    }
    TreeNode* buildTree(int l,int r){
       if(l>r) return NULL;
       int mid=(l+r)/2;
       TreeNode *p=new TreeNode(v[mid]);
       p->left=buildTree(l,mid-1);
       p->right=buildTree(mid+1,r);
       return p;
     }

8.环形链表

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

思想:

证明比较麻烦。记住快慢指针的流程即可。最终快指针比慢指针多跑的步数是圈数的整数倍。

   ListNode *detectCycle(ListNode *head) {
        ListNode *slow=head,*fast=head;
        while(fast){
            slow = slow->next;
            fast = fast->next;
            if(fast) fast =  fast->next;
            else break;
            if(slow==fast){
                slow = head;
                while(slow!=fast){
                    slow = slow->next;
                    fast = fast->next;
                }
                return slow;
            }
        }
        return NULL;
    }

9.对链表进行插入排序

给定单个链表的头 head ,使用 插入排序 对链表进行排序,并返回 排序后链表的头 。

插入排序 算法的步骤:

  1. 插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
  2. 每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
  3. 重复直到所有输入数据插入完为止。

思想:

思想与数组插入排序一致。h作为链表的头结点,初始化为空,遍历以head为头结点的链表,将每一个结点插入h结点后对应的位置。

    ListNode* insertionSortList(ListNode* head) {
        auto h = new ListNode(-1),pre = h,q=head;
        for(auto p = head; p; p=q){//把head的每个节点p插入到h链中
            for(pre = h; pre->next&&p->val>=pre->next->val;pre = pre->next){}//找插入点
            q = p->next,p->next = pre->next, pre->next = p;//插入
        }
        return h->next;
    }

10.链表的归并排序

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

思想:

思想与数组归并排序一致。首先将链表不断二分排序,直到满足边界条件,然后将有序的链表合并。同上。

    ListNode *sortList(ListNode *head){
        if (!head || !head->next) return head;
        auto s = head, f = head->next;
        //找到链表的中间位置
        while (f&&f->next){        //快慢指针,注意必须前两步存在
            s = s->next;
            f = f->next->next;
        }
        auto l1 = sortList(s->next);        //右链表
        s->next = NULL;        //将其断开,为两个链表
        auto l2 = sortList(head);
        return merge(l1, l2);
    }
    
    ListNode *merge(ListNode *l1, ListNode *l2){      //同上面的链表合并
        auto h = new ListNode(-1), r = h;//r为尾结点
        while(l1&&l2){
            if(l1->val < l2->val) r->next = l1,l1 = l1->next,r = r->next;//把l1连到尾部
            else r->next = l2,l2 = l2->next,r = r->next;
        }
        if(!l1) r->next = l2; 
        if(!l2) r->next = l1;
        return h->next;
    }

11.相交链表

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

思想:

链表的长度不一定相同,所以依次遍历找到相同结点是行不通的。注意到两个链表长度相交结点后的长度C相等,于是可以构造A+B-C=B+A-C,即同时遍历两个链表,遍历完后遍历另一个链表, 遍历到相同结点后终止,这样就能找到相交的起始结点。

    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        auto pa = headA,pb = headB;
        while(pa!=pb){
            pa = (pa ? pa->next:headB);
            pb = (pb ? pb->next:headA);
        }
        return pa;
    }

12.删除排序链表中的重复元素

给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。

思想:

遍历链表,关注当前元素和下一个元素,如果相等,则删除下一个元素,否则当前元素指向下一个元素。

    ListNode* deleteDuplicates(ListNode* h) {
        auto f=h;
        while(f){
            if(f->next&&f->next->val==f->val) f->next=f->next->next;
            else f=f->next;
        }
        return h;
    }

13.删除排序链表中的重复元素II

给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。

思想:

将不存在相同结点的结点插到头结点h后面,否则直接跳过。判别是否存在相同结点可以让与下一个结点值相同的结点后移,如果未后移则不存在相同结点,将结点加到尾节点t后。

    ListNode* deleteDuplicates(ListNode* head) {
        auto h=new ListNode(-1),t=h,p=head;
        while(p){
            auto q=p;
            while(p->next&&p->next->val==p->val) p=p->next;//发现相同前后结点相同后移
            if(q==p)//如果未后移则不存在相同结点,将结点加到尾节点t后
                t->next=p,t=t->next,p=p->next;
            else p=p->next;   否则跳过相同结点
        }
        t->next=NULL;尾指针赋为空
        return h->next;
    }
   }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值