首页 > 技术文章 > 笔试算法题:冒泡排序(Bubble Sort),二分查找,二叉树三种遍历等

feng-zhi 2021-06-14 10:32 原文

本文是记录自己练习leetcode算法题的一些思想并用代码实现,用来记录以及积累

1.冒泡排序(Bubble Sort):

  • 基本思想: 冒泡排序,类似于水中冒泡,较大的数沉下去,较小的数慢慢冒起来,假设从小到大,即为较大的数慢慢往后排,较小的数慢慢往前排。
  • 直观表达,每一趟遍历,将一个最大的数移到序列末尾。

算法描述:
比较相邻的元素,如果前一个比后一个大,交换之。
第一趟排序第1个和第2个一对,比较与交换,随后第2个和第3个一对比较交换,这样直到倒数第2个和最后1个,将最大的数移动到最后一位。
第二趟将第二大的数移动至倒数第二位
......
因此需要n-1趟;
动图实现,请看摘要的动图
冒泡排序

JAVA
    public static void bubbleSort(int[] arr) {

        for(int i =0 ; i<arr.length-1 ; i++) { 
          
            for(int j=0 ; j<arr.length-1-i ; j++) {  

                if(arr[j]>arr[j+1]) {
                    int temp = arr[j];
                    
                    arr[j]=arr[j+1];
                    
                    arr[j+1]=temp;
            }
            }    
        }
    }

运行截图:

2.二分查找

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列.

查找过程
首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

Java
public static int binarySearch(Integer[] srcArray, int des) {
    //定义初始最小、最大索引
    int start = 0;
    int end = srcArray.length - 1;
    //确保不会出现重复查找,越界
    while (start <= end) {
        //计算出中间索引值
        int middle = (end + start)/2 ;
        if (des == srcArray[middle]) {
            return middle;
        //判断下限
        } else if (des < srcArray[middle]) {
            end = middle - 1;
        //判断上限
        } else if(des > srcArray[middle]){
            start = middle + 1;
        }
    }
    //若没有,则返回-1
    return -1;
}

另外说明一点,计算 mid 时需要技巧防止溢出,建议写成: middle = start + (end - start) / 2

1. 为什么 while 循环的条件中是 <=,而不是 < ?

答:因为初始化 end 的赋值是 srcArray.length - 1,即最后一个元素的索引,而不是 srcArray.length。

这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 [start, end],后者相当于左闭右开区间 [start, end),因为索引大小为 srcArray.length 是越界的。

我们这个算法中使用的是 [start, end] 两端都闭的区间。这个区间就是每次进行搜索的区间,我们可以称为「搜索区间」(search space)

2. 为什么 start = middle + 1,end = middle - 1?

刚才明确了「搜索区间」这个概念,而且本算法的搜索区间是两端都闭的,即 [start,end]。那么当我们发现索引 middle 不是要找的 des时,如何确定下一步的搜索区间呢?

当然是去搜索 [start, mid - 1] 或者 [mid + 1, end] 对不对?因为 middle 已经搜索过,应该从搜索区间中去除。

3.二叉树的三种遍历

1. 先序遍历

先序遍历:按照根节点->左子树->右子树的顺序访问二叉树

先序遍历:
(1)访问根节点;
(2)采用先序递归遍历左子树;
(3)采用先序递归遍历右子树;
(注:每个节点的分支都遵循上述的访问顺序,体现“递归调用”)

先序遍历结果:A BDFE CGHI

2. 中序遍历

按照左子树->根节点->右子树的顺序访问

中序遍历:
(1)采用中序遍历左子树;
(2)访问根节点;
(3)采用中序遍历右子树

中序遍历结果:DBEF A GHCI

3. 后序遍历

按照左子树->右子树->根节点的顺序访问
后序遍历:

(1)采用后序递归遍历左子树;
(2)采用后序递归遍历右子树;
(3)访问根节点;
后序遍历的结果:DEFB HGIC A

网上发现了以为大佬写的更详细的解析,不懂的可以看他的文章

小结:三种方法遍历过程中经过节点的路线一样;只是访问各个节点的时机不同

4双指针类型题目:

4.1 两数之和 II - 输入有序数组

167两数之和 II - 输入有序数组原题
题目描述:在有序数组中找出两个数,使它们的和为 target。

使用双指针,一个指针指向值较小的元素,一个指针指向值较大的元素。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。

如果两个指针指向元素的和 sum == target,那么得到要求的结果;
如果 sum > target,移动较大的元素,使 sum 变小一些;
如果 sum < target,移动较小的元素,使 sum 变大一些。
数组中的元素最多遍历一次,时间复杂度为 O(N)。只使用了两个额外变量,空间复杂度为 O(1)。
代码实现如下

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        if(numbers==null)
        return null;
        int start=0;
        int end=numbers.length-1;
        while(start<end){
            int sum=numbers[start]+numbers[end];
            if(sum == target){
                return new int[]{start+1,end+1};
            }else if(sum>target){
                end--;
            }else if(sum<target){
                start++;
            }
        }
        return null;
    }
}

4.2.平方数之和

633. 平方数之和
题目描述:判断一个非负整数是否为两个整数的平方和。

可以看成是在元素为 0~target 的有序数组中查找两个数,使得这两个数的平方和为 target,如果能找到,则返回 true,表示 target 是两个整数的平方和。

本题和 167. Two Sum II - Input array is sorted 类似,只有一个明显区别:一个是和为 target,一个是平方和为 target。本题同样可以使用双指针得到两个数,使其平方和为 target。

本题的关键是右指针的初始化,实现剪枝,从而降低时间复杂度。设右指针为 x,左指针固定为 0,为了使 02 + x2 的值尽可能接近 target,我们可以将 x 取为 sqrt(target)。

因为最多只需要遍历一次 0~sqrt(target),所以时间复杂度为 O(sqrt(target))。又因为只使用了两个额外的变量,因此空间复杂度为 O(1)。
代码实现如下:

class Solution {
    public boolean judgeSquareSum(int c) {
        if(c<0)
        return false;
        int start=0;
        int end=(int)Math.sqrt(c);
        while (start<=end) {
            int powSum=start*start+end*end;
            if (powSum==c) {
               return true; 
            }else if (powSum>c) {
                end--;
            }else{
                start++;
            }
        }
        return false;
    }
}

4.3.反转字符串中的元音字符

345. 反转字符串中的元音字母
使用双指针,一个指针从头向尾遍历,一个指针从尾到头遍历,当两个指针都遍历到元音字符时,交换这两个元音字符。

为了快速判断一个字符是不是元音字符,我们将全部元音字符添加到集合 HashSet 中,从而以 O(1) 的时间复杂度进行该操作。

时间复杂度为 O(N):只需要遍历所有元素一次
空间复杂度 O(1):只需要使用两个额外变量

实现代码

class Solution {
    private final static HashSet<Character> word=new HashSet<Character>(Arrays.asList('a','e','i','o','u','A','E','I','O','U'));

    public String reverseVowels(String s) {
        if (s==null) {
            return null;
        }
        int start=0;
        int end=s.length()-1;
        char[] result=new char[s.length()];
        while(start<=end){
            char cStart=s.charAt(start);
            char cEnd=s.charAt(end);
            if (!word.contains(cStart)) {
                result[start++]=cStart;
            }
            else if (!word.contains(cEnd)) {
                result[end--]=cEnd;
            }else{
                result[start++]=cEnd;
                result[end--]=cStart;
            }
        }
        return new String(result);
    }
}

4.4.回文字符串

680. 验证回文字符串 Ⅱ
题目描述:可以删除一个字符,判断是否能构成回文字符串。

所谓的回文字符串,是指具有左右对称特点的字符串,例如 "abcba" 就是一个回文字符串。

使用双指针可以很容易判断一个字符串是否是回文字符串:令一个指针从左到右遍历,一个指针从右到左遍历,这两个指针同时移动一个位置,每次都判断两个指针指向的字符是否相同,如果都相同,字符串才是具有左右对称性质的回文字符串。

本题的关键是处理删除一个字符。在使用双指针遍历字符串时,如果出现两个指针指向的字符不相等的情况,我们就试着删除一个字符,再判断删除完之后的字符串是否是回文字符串。

在判断是否为回文字符串时,我们不需要判断整个字符串,因为左指针左边和右指针右边的字符之前已经判断过具有对称性质,所以只需要判断中间的子字符串即可。

在试着删除字符时,我们既可以删除左指针指向的字符,也可以删除右指针指向的字符。
代码实现

class Solution {
    public boolean validPalindrome(String s) {
        for(int i=0,j=s.length()-1;i<j;i++,j--){
            if (s.charAt(i)!=s.charAt(j)) {
                return isPailindrome(s,i,j-1)|| isPailindrome(s,i+1,j);
            }
        }
        return true;
    }
    public boolean isPailindrome(String s,int i,int j){
        while(i<j){
            if (s.charAt(i++)!=s.charAt(j--)) {
                return false;
            }
        }
        return true;
    }
}

4.5合并两个有序数组

88. 合并两个有序数组

思路:
设定两个指针分别位于两个数组的末端,初始化index为nums1数组的最后一处索引,从此处开始保存数据。
当两个数组的指针都没有倒数完的时候,分别判断哪个大,保存至值,并更新指针的位置和index的位置。
当有一个数组倒数完的时候,说明剩下那个数组的所有数字可以直接按顺序保存过去,倒数情况下不用考虑nums1,因为数值本身就在nusm1里面,如果nums2还没有倒数完,则按顺序将对应值保存到对应index位置。

代码实现:

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int index1=m-1;
        int index2=n-1;
        int indexMerge=m+n-1;
        while (index2>=0 && index1>=0) {
            if (nums2[index2]>=nums1[index1]) {
                nums1[indexMerge--]=nums2[index2--];
            }else{
                nums1[indexMerge--]=nums1[index1--];
            }
        }
        while (index2>=0) {
            nums1[indexMerge--]=nums2[index2--];
        }
    }
}

4.6判断链表是否存在环

141.环形链表

思路:使用双指针,一个指针每次移动一个节点,一个指针每次移动两个节点,如果存在环,那么这两个指针一定会相遇。
实现代码:

public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head==null) {
            return false;
        }
        ListNode l1=head;
        ListNode l2=head.next;
        while (l1!=null && l2!=null && l2.next!=null) {
            if (l1==l2) {
                return true;
            }
            l1=l1.next;
            l2=l2.next.next;
        }
        return false;
    }
}

4.7. 最长子序列

524. 通过删除字母匹配到字典里最长单词

思路:通过删除字符串 s 中的一个字符能得到字符串 t,可以认为 t 是 s 的子序列,我们可以使用双指针来判断一个字符串是否为另一个字符串的子序列。
代码实现:

class Solution {
    public String findLongestWord(String s, List<String> dictionary) {
        String longestWord="";
        for(String target:dictionary){
            int l1=longestWord.length();
            int l2=target.length();
            if (l1>l2||(l1==l2&&longestWord.compareTo(target)<0)) {
                continue;
            }
            if (isSubstr(s,target)) {
                longestWord=target;
            }
        }
        return longestWord;
    }
    private boolean isSubstr(String s,String target){
        int i=0;int j=0;
        while (i<s.length()&&j<target.length()) {
            if (s.charAt(i)==target.charAt(j)) {
                j++;
            }
            i++;
        }
        return j==target.length();
    }
}

本文只归纳一些常见的笔试题,供萌新的自己学习.

推荐阅读