【算法】牛客网算法初级班(栈、队列、链表、数组和矩阵结构介绍及常见面试题讲解(下))

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ARPOSPF/article/details/81909296

栈、队列、链表、数组和矩阵结构介绍及常见面试题讲解(下)


反转单向和双向链表

题目:

分布实现反转单向链表和反转双向链表的函数

要求:

如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1).

代码:

/**
 * 反转单向和双向链表
 */
public class ReverseList {
    public class Node {
        public int value;
        public Node next;

        public Node(int data) {
            this.value = data;
        }
    }

    public class DoubleNode {
        int value;
        DoubleNode last;
        DoubleNode next;

        public DoubleNode(int data) {
            this.value = data;
        }
    }

    /**
     * 反转单向链表
     *
     * @param head
     * @return
     */
    public Node reverseList(Node head) {
        Node pre = null;
        Node next = null;
        while (head != null) {
            next = head.next;
            head.next = pre;
            pre = head;
            head = next;
        }
        return pre;
    }

    /**
     * 反转双向链表
     * @param head
     * @return
     */
    public DoubleNode reverseList(DoubleNode head) {
        DoubleNode pre = null;
        DoubleNode next = null;
        while (head != null) {
            next = head.next;
            head.next = pre;
            head.last = next;
            pre = head;
            head = next;
        }
        return pre;
    }
}

打印两个有序链表的公共部分

题目:

给定两个有序链表的头指针head1和head2,打印两个链表的公共部分。

思路:

因为是有序链表,所以从两个链表的头开始进行如下判断:

  1. 如果head1的值小于head2,则head1往下移动。
  2. 如果head2的值小于head1,则head2往下移动。
  3. 如果head1的值与head2的值相等,则打印这个值,然后head1和head2都往下移动。
  4. head1或head2有任何一个移动到null,这个过程停止。

代码:

/**
 * 打印两个有序链表的公共部分
 */
public class PrintCommonPart {
    public class Node {
        int value;
        Node next;

        public Node(int data) {
            this.value = data;
        }
    }

    public static void printCommonPart(Node head1, Node head2) {
        System.out.print("Common Part:");
        while (head1 != null && head2 != null) {
            if (head1.value < head2.value) {
                head1 = head1.next;
            } else if (head1.value > head2.value) {
                head2 = head2.next;
            } else {
                System.out.print(head1.value + " ");
                head1 = head1.next;
                head2 = head2.next;
            }
        }
        System.out.println();
    }
}

判断一个链表是否为回文结构

题目:

给定一个链表的头节点head,请判断该链表是否为回文结构。

例如:

1->2->1,返回true.

1->2->2->1,返回true

15->6->15,返回true

1->2->3,返回false

进阶:

如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1).

思路:

方法一:利用栈结构,从左到由遍历链表,遍历的过程中把每个节点依次压入栈中。因为栈是后进先出的,所以在遍历完成后,从栈顶到栈底的节点值出现顺序会与原链表从左到右的值出现顺序反过来。那么,如果一个链表是回文结构,逆序之后,值出现的次序还是一样的,如果不是回文结构,顺序就肯定对不上。需要的额外空间复杂度为O(N)。

方法二:也是利用栈结构,但是并不需要将所有的节点都压入栈中,只用压入一半的节点即可。首先假设链表的长度为N,如果N是偶数,前N/2的节点叫左半区,后N/2的节点叫右半区。如果N是奇数,忽略处于最中间的节点,还是前N/2的节点叫左半区,后N/2的节点叫右半区。把整个链表的右半部分压入栈中,压入完成后,再检查栈顶到栈底值出现的顺序是否和链表左半部分的值相对应。

方法三:不需要栈和其他数据结构,只用有限几个变量。其额外空间复杂度为O(1),时间复杂度为O(N)。具体过程为:

  1. 首先改变链表右半区的结构,使整个右半区反转,最后指向中间节点。
  2. 将左半区的第一个节点(也就是原链表的头节点)记为leftStart,右半区反转之后最右边的节点(也就是原链表的最后一个节点)记为rightStart。leftStart和rightStart同时向中间点移动,移动每一步都比较leftStart和rightStart节点的值,看是否一样。如果都一样,说明链表为回文结构,否则不是回文结构。
  3. 不管最后返回的是true还是false,在返回前都应该把链表恢复成原来的样子。
  4. 链表恢复成原来的结构之后,返回检查结果。

代码:

import java.util.Stack;

/**
 * 判断一个链表是否为回文结构
 */
public class IsPalindromeList {
    public class Node {
        int value;
        public Node next;

        public Node(int data) {
            this.value = data;
        }
    }

    /**
     * 方法1:利用栈结构,从左到右遍历链表,遍历的过程中把每个节点依次压入栈中.
     *
     * @param head
     * @return
     */
    public boolean isPalindrome1(Node head) {
        Stack<Node> stack = new Stack<>();
        Node curr = head;
        while (curr != null) {
            stack.push(curr);
            curr = curr.next;
        }
        while (head != null) {
            if (head.value != stack.pop().value) {
                return false;
            }
            head = head.next;
        }
        return true;
    }

    /**
     * 方法2:将这个链表的右半部分压入栈中,压入完成后,再检查栈顶到栈底值出现的顺序是否和链表左半部分的值相对应
     *
     * @param head
     * @return
     */
    public boolean isPalindrome2(Node head) {
        if (head == null || head.next == null) {
            return true;
        }
        Node right = head.next;
        Node curr = head;
        while (curr.next != null && curr.next.next != null) {
            right = right.next;
            curr = curr.next.next;
        }
        Stack<Node> stack = new Stack<>();
        while (right != null) {
            stack.push(right);
            right = right.next;
        }
        while (!stack.isEmpty()) {
            if (head.value != stack.pop().value) {
                return false;
            }
            head = head.next;
        }
        return true;
    }

    /**
     * 方法三:将链表右半部分的结构改变,使整个右半区反转,最后指向中间节点
     * 时间复杂度为O(N),额外空间复杂度为O(1)
     * @param head
     * @return
     */
    public boolean isPalindrome3(Node head) {
        if (head == null || head.next == null) {
            return true;
        }
        Node n1 = head;//慢指针
        Node n2 = head;//快指针
        while (n2.next != null && n2.next.next != null) {//查找中间节点
            n1 = n1.next;//n1->中部
            n2 = n2.next.next;//n2->尾部
        }
        //调整右半部分的指向
        n2 = n1.next;//n2->右部分第一个节点
        n1.next = null;//mid.next->null
        Node n3 = null;
        while (n2 != null) {//右半部分反转
            n3 = n2.next;//n3->保存下一个节点
            n2.next = n1;//下一个反转节点
            n1 = n2;//n1移动
            n2 = n3;//n2移动
        }
        n3 = n1;//n3->保存最后一个节点
        n2 = head;//n2->左边第一个节点
        boolean res = true;
        while (n1 != null && n2 != null) {//检查回文
            if (n1.value != n2.value) {
                res = false;
                break;
            }
            n1 = n1.next;//从左到中部
            n2 = n2.next;//从右到中部
        }
        n1 = n3.next;
        n3.next = null;
        while (n1 != null) {//恢复列表
            n2 = n1.next;
            n1.next = n3;
            n3 = n1;
            n1 = n2;
        }
        return res;
    }
}

将单向链表按某值划分成左边小、中间相等、右边大的形式

题目:

给定一个单向链表的头节点head,节点的值类型是整型,再给定一个整数pivot。实现一个调整链表的函数,将链表调整为左部分的值小于pivot的节点,中间部分都是值等于pivot的节点,右部分都是值大于pivot的节点。除这个要求外,对调整后的节点顺序没有更多的要求。

例如:链表9->0->4->5->1,pivot=3

调整后链表可以是1->0->4->9->5,也可以是0->1->9->5->4。总之,满足左部分都是小于3的节点,中间部分都是等于3的节点,有部分都是大于3的节点即可,对某部分内部的节点顺序不做要求。

进阶:

在原问题的要求之上再增加如下两个要求:

  • 在左、中、右三个部分的内部也做顺序要求,要求每部分里的节点从左到右的顺序与原链表中节点的先后顺序一致。

例如:链表9->0->4->5->1,pivot=3,调整后的链表是0->1->9->4->5。在满足原问题要求的同时,左部分节点从左到右为0、1。在原链表中也是先出现0,后出现1;中间部分在本例中为空;右部分节点从左到右为9,4,5。在原链表中也是先出现9,然后出现4,最后出现5。

  • 如果链表长度为N,时间复杂度请达到O(N),额外空间复杂度为O(1)。

思路:

普通解法的时间复杂度为O(N),额外空间复杂度为O(N),就是把链表中的所有节点放入一个额外的数组中,然后统一调整位置的办法。具体过程为:

  1. 先遍历一遍链表,为了得到链表的长度,假设长度为N。
  2. 生成长度为N的Node类型的数组nodeArr,然后遍历一次链表,将节点依次放入nodeArr中。
  3. 在nodeArr中把小于pivot的节点放在左边,把相等的放在中间,把大于的放在右边。也就是改进版的快速排序中partition的调整过程。
  4. 经过步骤3的调整后,nodeArr是满足题目要求的节点顺序,只要把nodeArr中的节点依次重连接即可,整个过程结束。

进阶解法的具体过程如下:

  1. 将原链表中的所有节点依次划分进三个链表,三个链表分别为small代表左部分、equal代表中间部分、big代表右部分。
  2. 将small、equal和big三个链表重新串起来即可。
  3. 整个过程需要特别注意对null节点的判断和处理。

代码:

/**
 * 将单向链表按某值划分成左边小、中间相等、右边大的形式
 */
public class SmallerEqualBigger {
    public class Node {
        int value;
        public Node next;

        public Node(int data) {
            this.value = data;
        }
    }

    /**
     * 普通解法,时间复杂度为O(N),额外空间复杂度为O(N)
     *
     * @param head
     * @param pivot
     * @return
     */
    public Node listPartition1(Node head, int pivot) {
        if (head == null) {
            return head;
        }
        Node curr = head;
        int i = 0;
        while (curr != null) {//得到链表的长度
            i++;
            curr = curr.next;
        }
        Node[] nodeArr = new Node[i];
        i = 0;
        curr = head;
        for (i = 0; i != nodeArr.length; i++) {//将节点依次放进nodeArr中
            nodeArr[i] = curr;
            curr = curr.next;
        }
        //使用partition过程按要求调整数组
        arrPartition(nodeArr, pivot);
        for (i = 1; i != nodeArr.length; i++) {
            nodeArr[i - 1].next = nodeArr[i];
        }
        nodeArr[i - 1].next = null;
        return nodeArr[0];
    }

    private void arrPartition(Node[] nodeArr, int pivot) {
        int small = -1;
        int big = nodeArr.length;
        int index = 0;
        while (index != big) {
            if (nodeArr[index].value < pivot) {
                swap(nodeArr, ++small, index++);
            } else if (nodeArr[index].value == pivot) {
                index++;
            } else {
                swap(nodeArr, --big, index);
            }
        }
    }

    private void swap(Node[] nodeArr, int a, int b) {
        Node temp = nodeArr[a];
        nodeArr[a] = nodeArr[b];
        nodeArr[b] = temp;
    }

    /**
     * 进阶解法:使用三个链表分别表示小于、等于、大于的部分
     * 时间复杂度为O(N),额外空间复杂度为O(1)
     * @param head
     * @param pivot
     * @return
     */
    public static Node listPartition2(Node head, int pivot) {
        Node sH = null;//小于部分的头
        Node sT = null;//小于部分的尾
        Node eH = null;//相等部分的头
        Node eT = null;//相等部分为尾
        Node bH = null;//大于部分的头
        Node bT = null;//大于部分为尾
        Node next = null;//保存下一个节点
        //所有的节点分进三个链表中
        while (head != null) {
            next = head.next;
            head.next = null;
            if (head.value < pivot) {
                if (sH == null) {
                    sH = head;
                    sT = head;
                } else {
                    sT.next = head;
                    sT = head;
                }
            } else if (head.value == pivot) {
                if (eH == null) {
                    eH = head;
                    eT = head;
                } else {
                    eT.next = head;
                    eT = head;
                }
            } else {
                if (bH == null) {
                    bH = head;
                    bT = head;
                } else {
                    bT.next = head;
                    bT = head;
                }
            }
            head = next;
        }
        //小的和相等的重新连接
        if (sT != null) {
            sT.next = eH;
            eT = eT == null ? sT : eT;
        }
        //所有的重新连接
        if (eT != null) {
            eT.next = bH;
        }
        return sH != null ? sH : eH != null ? eH : bH;
    }
}

复制含有随机指针节点的链表

题目:

一种特殊的链表节点类描述如下:

public class Node{
    public int value;
    public Node next;
    public Node rand;
    public Node(int data){
        this.value = data;
    }
}

Node类中的value是节点值,next指针和正常单链表中的next指针的意义一样,都指向下一个节点,rand指针是Node类中新增的指针,这个指针可能指向量表中任意一个节点,也可能指向null。

给定一个由Node节点类型组成的无环单链表的头结点head,请实现一个函数完成这个链表中所有结构的复制,并返回复制的新链表的头节点。

进阶:不使用额外的数据结构,只用有限几个变量,且在时间复杂度为O(N)内完成原问题要实现的函数。

思路:

普通解法:时间复杂度为O(N),额外空间复杂度为O(N),需要使用到哈希表(HashMap)结构,一共只遍历链表两遍。具体过程如下:

  1. 首先从左到由遍历链表,对每个节点都复制生成相应的副本节点,然后将对应关系放入哈希表map中。在步骤1完成后,原链表没有任何变化,每一个副本节点的next和rand指针都指向null。
  2. 再从左到右遍历链表,此时就可以设置每一个副本节点的next和rand指针。
  3. 将1’节点作为结果返回即可。

进阶解法:不使用哈希表保存对应关系,只使用有限的几个变量。具体过程如下:

  1. 首先从左到右遍历链表,对每个节点cur都复制生成相应的副本节点copy,然后把copy放在cur和下一个要遍历节点的中间。
  2. 再从左到右遍历链表,在遍历时设置每一个副本节点的rand指针。
  3. 步骤2完成后,节点1,2,3,...之间的rand关系没有任何变化,节点1',2',3',...之间的rand关系也被正确设置了,此时所有的节点与副本节点串在一起,将其分离出来即可。
  4. 将1'节点作为结果返回即可。

代码:

import java.util.HashMap;

/**
 * 复制含有随机节点的链表
 */
public class CopyListWithRand {
    public class Node {
        int value;
        Node next;
        Node rand;

        public Node(int data) {
            this.value = data;
        }
    }

    /**
     * 普通解法:使用hashmap结构,时间复杂度为O(N),空间复杂度为O(N)
     *
     * @param head
     * @return
     */
    public Node copyListWithRand1(Node head) {
        HashMap<Node, Node> map = new HashMap<>();
        Node cur = head;
        while (cur != null) {
            map.put(cur, new Node(cur.value));
            cur = cur.next;
        }
        cur = head;
        while (cur != null) {
            map.get(cur).next = map.get(cur.next);
            map.get(cur).rand = map.get(cur.rand);
            cur = cur.next;
        }
        return map.get(head);
    }

    /**
     * 进阶解法
     * @param head
     * @return
     */
    public Node copyListWithRand2(Node head) {
        if (head == null) {
            return null;
        }
        Node cur = head;
        Node next = null;
        //复制并连接每一个节点
        while (cur != null) {
            next = cur.next;//保存下一个节点
            cur.next = new Node(cur.value);//创建当前节点的副本
            cur.next.next = next;//将下一个节点设置为下下一个节点
            cur = next;//将指针转移到下下一个节点上
        }
        cur = head;
        Node curCopy = null;
        //设置复制节点的rand指针
        while (cur != null) {
            next = cur.next.next;//取原链表的节点
            curCopy = cur.next;//取复制的副本节点
            curCopy.rand = cur.rand != null ? cur.rand.next : null;//复制随机指针
            cur = next;
        }
        Node res = head.next;//新链表的头部
        cur = head;//原链表头部
        //拆分
        while (cur != null) {
            next = cur.next.next;//原链表的节点
            curCopy = cur.next;//新链表的节点
            cur.next = next;//将当前节点的下一个节点设置为next,即链表的第三个节点
            curCopy.next = next != null ? next.next : null;//将新链表的下一个节点设置为next节点的下一个
            cur = next;//转移当前节点到下下一个节点上
        }
        return res;

    }
}

两个单链表相交的一系列问题

题目:

在本题中,单链表可能有环、也可能无环。给定两个单链表的头节点head1和head2,这两个链表可能相交、也可能不相交,请实现一个函数,如果两个链表相交,请返回相交的第一个节点,如果不相交,返回null即可。

要求:如果链表1的长度为N,链表2的长度为M,时间复杂度请达到O(N+M),额外空间复杂度请达到O(1)。

思路:

本题可以拆分成三个子问题,每个问题都可以作为一道独立的算法题,具体如下:

  • 问题一:如何判断一个链表是否有环?如果有,则返回第一个进入环的节点,没有则返回null;
  • 问题二:如何判断两个无环链表是否相交?相交则返回第一个相交的节点,不相交则返回null;
  • 问题三:如何判断两个有环链表是否相交?相交则返回第一个相交的节点,不相交则返回null;

另外,如果一个链表有环,另外一个链表无环,它们是不可能相交的,直接返回null。

对于问题一,如果一个链表没有环,那么遍历链表一定可以遇到链表的终点;如果链表有环,那么遍历链表就永远在环里面转下去。如何找到第一个入环节点,具体过程如下:

  1. 设置一个慢指针slow和一个快指针fast。在开始时,slow和fast都指向链表的头节点head。然后slow每次移动一步,fast每次移动两步,在链表中遍历起来。
  2. 如果链表无环,那么fast指针在移动过程中一定先遇到终点,一旦fast到达终点,说明链表是没有环的,直接返回null,表示该链表无环,当然也没有第一个入环的节点。
  3. 如果链表有环,那么fast指针和slow指针一定会在环中的某个位置相遇,当fast和slow相遇时,fast指针重新回到head的位置,slow指针不动。接下来,fast指针从每次移动两步改为每次移动一步,slow指针依然每次移动一步,然后继续遍历。
  4. fast指针和slow指针一定再次相遇,并且在第一个入环的节点处相遇。

能相交的情况分为两种:一种是两个链表都是无环,即问题二,另一种情况是两个链表都有环,即问题三。

对于问题二:如果两个无环链表相交,那么从相交节点开始,一直到两个链表终止的这一段,是两个链表共享的。具体过程如下:

  1. 链表1从头节点开始,走到最后一个节点(不是结束),统计链表1的长度记为len1,同时记录链表1的最后一个节点记为end1。
  2. 链表2从头节点开始,走到最后一个节点(不是结束),统计链表2的长度为len2,同时记录链表2的最后一个节点记为end2。
  3. 如果end1!=end2,说明两个链表不相交,返回null即可;如果end1==end2,说明两个链表相交,进入步骤4来寻找第一个相交节点。
  4. 如果链表1比较长,链表1就先走len1-len2步;如果链表2比较长,链表2就先走len2-len1步。然后两个链表一起走,一起走的过程中,两个链表第一次走到一起的那个节点,就是第一个相交的节点。

考虑问题三的时候,已经得到了两个链表各自的第一个入环节点,假设链表1的第一个入环节点记为loop1;链表2的第一个入环节点记为loop2.

  1. 如果loop1==loop2,那么两个链表的结构为共用入环节点。这种情况下,只需要考虑链表1从头开始到loop1这一段与链表2从头开始到loop2这一段,在哪里第一次相遇即可,而不用考虑进环该怎么处理。这里把loop1(loop2)作为链表的终点,判断的主要过程与问题二相同。
  2. 如果loop1!=loop2,则链表1和链表2可能相交或者不相交,此时进入步骤3.
  3. 让链表1从loop1出发,因为loop1和之后的所有节点都在环上,所以将来一定能回到loop1。如果回到loop1之前没有遇到loop2,说明两个链表不相交,直接返回null;如果回到loop1之前遇到loop2,说明两个链表相交。因为loop1和loop2都在两条链表上,只不过loop1是离链表1较近的节点,loop2是离链表2较近的节点。所以此时返回loop1或loop2都可以。

代码:

/**
 * 两个单链表相交的一系列问题
 */
public class FindFirstIntersectNode {
    public class Node {
        int value;
        public Node next;

        public Node(int data) {
            this.value = data;
        }
    }

    public Node getLoopNode(Node head) {
        if (head == null || head.next == null || head.next.next == null) {
            return null;
        }
        Node n1 = head.next;//n1->慢指针
        Node n2 = head.next.next;//n2->快指针
        while (n1 != n2) {
            if (n2.next == null || n2.next.next == null) {
                return null;
            }
            n2 = n2.next.next;
            n1 = n1.next;
        }
        n2 = head;//n2回到头结点,再次遍历
        while (n1 != n2) {
            n1 = n1.next;
            n2 = n2.next;
        }
        return n1;//当n1和n2再次相遇即为第一个入环节点
    }

    public Node noLoop(Node head1, Node head2) {
        if (head1 == null || head2 == null) {
            return null;
        }
        Node cur1 = head1;//链表1的长度
        Node cur2 = head2;//链表2的长度
        int n = 0;
        while (cur1.next != null) {
            n++;
            cur1 = cur1.next;
        }
        while (cur2.next != null) {
            n--;
            cur2 = cur2.next;
        }
        if (cur1 != cur2) {
            return null;
        }
        cur1 = n > 0 ? head1 : head2;
        cur2 = cur1 == head1 ? head2 : head1;
        n = Math.abs(n);//两个链表长度的差值
        while (n != 0) {
            n--;
            cur1 = cur1.next;
        }
        //当两个链表第一次走到一起的那个节点,就是第一个相交的节点
        while (cur1 != cur2) {
            cur1 = cur1.next;
            cur2 = cur2.next;
        }
        return cur1;
    }

    public Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
        Node cur1 = null;
        Node cur2 = null;
        //如果两个链表的入环节点相同,则只需比较从链表头到入环节点之间的节点
        if (loop1 == loop2) {
            cur1 = head1;
            cur2 = head2;
            int n = 0;
            while (cur1 != loop1) {
                n++;
                cur1 = cur1.next;
            }
            while (cur2 != loop2) {
                n--;
                cur2 = cur2.next;
            }
            cur1 = n > 0 ? head1 : head2;
            cur2 = cur1 == head1 ? head2 : head1;
            while (n != 0) {
                n--;
                cur1 = cur1.next;
            }
            while (cur1 != cur2) {
                cur1 = cur1.next;
                cur2 = cur2.next;
            }
            return cur1;
        }//否则,让链表1从loop1出发,如果回到loop1之前遇到loop2,则说明两个在环上相交,返回loop1或loop2都可以
        else {
            cur1 = loop1.next;
            while (cur1 != loop1) {
                if (cur1 == loop2) {
                    return loop1;
                }
                cur1 = cur1.next;
            }
            return null;
        }
    }

    public Node getIntersectNode(Node head1, Node head2) {
        if (head1 == null || head2 == null) {
            return null;
        }
        Node loop1 = getLoopNode(head1);//链表1的是否有环
        Node loop2 = getLoopNode(head2);//链表2是否有环
        if (loop1 == null && loop2 == null) {
            return noLoop(head1, head2);
        }
        if (loop1 != null && loop2 != null) {
            return bothLoop(head1, loop1, head2, loop2);
        }
        return null;
    }
}

在数组中找到一个局部最小的位置

题目:

给定无序数组arr,已知arr中任意两个相邻的数都不相等。写一个函数,只需返回arr中任意一个局部最小出现的位置即可。

定义局部最小的概念。arr的长度为1时,arr[0]是局部最小。arr的长度为N(N>1)时,如果arr[0]<arr[1],那么arr[0]是局部最小;如果arr[N-1]<arr[N-2],那么arr[N-1]是局部最小;如果0<i<N-1,既有arr[i]<arr[i-1],又有arr[i]<arr[i+1],那么arr[i]是局部最小。

思路;

可以采用二分查找做到时间复杂度为O(logN),额外空间复杂度为O(1)。具体过程如下:

  1. 如果arr为空或者长度为0,返回-1表示不存在局部最小。
  2. 如果arr长度为1或者arr[0]<arr[1],说明arr[0]是局部最下,返回0.
  3. 如果arr[N-1]<arr[N-2],说明arr[N-1]是局部最小,返回N-1.
  4. 如果arr长度大于2且arr的左右两头都不是局部最小,则令left=1,right=N-2,然后进入步骤5做二分查找。
  5. 令mid=(left+right)/2,然后进行如下判断:
  • 如果arr[mid]>arr[mid-1],可知arr[left....mid-1]上肯定存在局部最小,令right=mid-1,重复步骤5.
  • 如果不满足上一条,但arr[mid]>arr[mid+1],可知在arr[mid+1.....right]上肯定存在局部最小,令left=mid+1,重复步骤5.
  • 如果不满足上面两条,那么arr[mid]就是局部最小,直接返回mid。
  • 步骤5一直进行二分查找,直到left==right时停止,返回left即可。

可见,二分查找并不是数组有序时才能使用,只要能确定二分两侧的某一测肯定存在要找的内容,就可以使用二分查找。

代码:

/**
 * 在数组中找到一个局部最小的位置
 */
public class FindLessIndex {
    /**
     * 利用二分查找,时间复杂度为O(logN),额外空间复杂度为O(1)
     *
     * @param arr
     * @return
     */
    public int getLessIndex(int[] arr) {
        if (arr == null || arr.length == 0) {
            return -1;//不存在
        }
        if (arr.length == 1 || arr[0] < arr[1]) {
            return 0;
        }
        if (arr[arr.length - 1] < arr[arr.length - 2]) {
            return arr.length - 1;
        }
        int left = 1;
        int right = arr.length - 2;
        int mid = 0;
        while (left < right) {
            mid = (left + right) / 2;
            if (arr[mid] > arr[mid - 1]) {
                right = mid - 1;
            } else if (arr[mid] > arr[mid + 1]) {
                left = mid + 1;
            } else {
                return mid;
            }
        }
        return left;
    }
}

猜你喜欢

转载自blog.csdn.net/ARPOSPF/article/details/81909296