大数据面试题-算法题

目录

1.时间复杂度、空间复杂度理解

2.常见算法求解思想

3.基本算法

3.1冒泡排序

3.2 快速排序

3.3  归并排序

3.4 遍历二叉树

3.5 二分查找

3.6 小青蛙跳台阶

3.7  最长回文子串

3.8 数字字符转化成IP


 

1.时间复杂度、空间复杂度理解

在计算机算法理论中,用时间复杂度和空间复杂度来分别从这两方面衡量算法的性能。

1)时间复杂度(Time Complexity)

算法的时间复杂度,是指执行算法所需要的计算工作量。

一般来说,计算机算法是问题规模n 的函数fn,算法的时间复杂度也因此记做:Tn= Οfn))。

问题的规模n 越大,算法执行的时间的增长率与fn的增长率正相关,称作渐进时间复杂度(Asymptotic Time Complexity)。

2)空间复杂度

算法的空间复杂度,是指算法需要消耗的内存空间。有时候递归调用,还需要考虑调用栈所占用的空间。

其计算和表示方法与时间复杂度类似,一般都用复杂度的渐近性来表示。同时间复杂度相比,空间复杂度的分析要简单得多。

所以,我们一般对程序复杂度的分析,重点都会放在时间复杂度上。

2.常见算法求解思想

1)暴力求解

不推荐。

2)动态规划

动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。

动态规划过程是:把原问题划分成多个“阶段”,依次来做“决策”,得到当前的局部解;每次决策,会依赖于当前“状态”,而且会随即引起状态的转移。

这样,一个决策序列就是在变化的状态中,“动态”产生出来的,这种多阶段的、最优化决策,解决问题的过程就称为动态规划(Dynamic Programming,DP)。

3)分支

对于复杂的最优化问题,往往需要遍历搜索解空间树。最直观的策略,就是依次搜索当前节点的所有分支,进而搜索整个问题的解。为了加快搜索进程,我们可以加入一些限制条件计算优先值,得到优先搜索的分支,从而更快地找到最优解:这种策略被称为“分支限界法”。

分支限界法常以广度优先(BFS)、或以最小耗费(最大效益)优先的方式,搜索问题的解空间树。

3.基本算法

3.1冒泡排序

冒泡排序是一种简单的排序算法。

它的基本原理是:重复地扫描要排序的数列,一次比较两个元素,如果它们的大小顺序错误,就把它们交换过来。这样,一次扫描结束,我们可以确保最大(小)的值被移动到序列末尾。这个算法的名字由来,就是因为越小的元素会经由交换,慢慢“浮”到数列的顶端。

冒泡排序的时间复杂度为O(n2)。

public void bubbleSort(int nums[]) {
    int n = nums.length; // 获取数组的长度

    // 外层循环,表示总共需要进行n-1轮比较
    for(int i = 0; i < n - 1; i++) {

        // 内层循环,表示每轮需要比较的次数,注意这里是n-i-1
        // 因为每进行一轮比较,最大的数就会被排到最后,所以每轮比较的次数都会减1
        for(int j = 0; j < n - i - 1; j++) {

            // 这里是冒泡排序的核心,比较相邻两个元素的大小
            if(nums[j + 1] < nums[j])

                // 如果后一个元素小于前一个元素,那么就交换他们的位置
                // 这就是为什么叫做"冒泡",大的数会被逐渐"冒"到数组的末尾
                swap(nums, j, j + 1);
        }
    }
}


public void swap(int[] nums, int i, int j) {
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}

 2.为什么冒泡排序的时间复杂度为O(n² )?

冒泡排序的时间复杂度是O(n²)是因为这个算法的工作原理。在冒泡排序中,我们比较数组中的每一对相邻元素,并将它们交换位置,如果第一个元素比第二个元素大。

首先,如果我们有n个元素,那么在第一轮中,我们需要进行n-1次比较。然后在第二轮中,我们需要进行n-2次比较,依此类推,直到最后一轮只需要进行一次比较。因此,总的比较次数为(n-1) + (n-2) + ... + 2 + 1,这是一个等差数列,其和可以表示为n*(n-1)/2。

这个表达式可以简化为1/2 * n² - 1/2 * n,其中n²的项是主导项,因此,我们说冒泡排序的时间复杂度是O(n²)。这意味着,如果输入数组的大小翻倍,那么所需的时间将会增加四倍。

请注意,这是最坏情况下的时间复杂度,也就是说,当输入数组是逆序排列的时候。在最好的情况下(也就是数组已经是排序好的),冒泡排序的时间复杂度是O(n)。但是,我们通常关注的是最坏情况的时间复杂度,因为这给出了算法在最不利情况下的性能上限。

3.2 快速排序

 快速排序的基本思想:通过一趟排序,将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

快排应用了分治思想,一般会用递归来实现。

快速排序的时间复杂度可以做到O(nlogn),在很多框架和数据结构设计中都有广泛的应用。

1.实现1 

这是一个使用分治策略的快速排序算法的Java实现。它包含两个函数:qSort用于递归地进行快速排序,partition用于实现数组的分区。以下是详细的代码注释:

public void qSort(int[] nums, int start, int end){
    // 如果起始索引大于等于结束索引,说明只剩下一个元素或者没有元素,直接返回
    if (start >= end) return;
    
    // 调用partition函数,返回枢轴元素的最终位置
    int mid = partition(nums, start, end);

    // 对枢轴元素左侧的子数组进行快速排序
    qSort(nums, start, mid - 1);
    
    // 对枢轴元素右侧的子数组进行快速排序
    qSort(nums, mid + 1, end);
}

// 定义分区方法,把数组按一个基准划分两部分,左侧元素一定小于基准,右侧大于基准
private static int partition( int[] nums, int start, int end ){
    // 以当前数组起始元素为pivot
    int pivot = nums[start];
    int left = start; 
    int right = end; 

    // 当左指针小于右指针时,进行循环
    while ( left < right ){
        // 当右指针所指元素大于等于pivot时,右指针左移
        while ( left < right && nums[right] >= pivot )
            right --;

        // 将右指针所指元素赋值给左指针所指位置
        nums[left] = nums[right];

        // 当左指针所指元素小于等于pivot时,左指针右移
        while ( left < right && nums[left] <= pivot )
            left ++;

        // 将左指针所指元素赋值给右指针所指位置
        nums[right] = nums[left];
    }

    // 当左右指针相遇时,将枢轴元素放到最终的正确位置
    nums[left] = pivot;

    // 返回枢轴元素的索引
    return left;
}

这段代码实现了一个快速排序算法,它首先通过partition函数将数组划分为两部分,左侧部分包含所有小于枢轴元素的值,右侧部分包含所有大于枢轴元素的值。然后,递归地对左侧和右侧部分分别进行快速排序,直到整个数组被排序。

2.实现2

你可以使用迭代的方法配合栈数据结构来实现快速排序,从而避免使用递归。以下是实现的代码及注释:

public void quickSort(int[] nums) {
    if(nums.length <= 1) return; // 如果数组长度小于等于1,直接返回

    // 创建一个栈用于保存子数组的起始和结束索引
    Stack<Integer> stack = new Stack<>();

    // 初始状态下,将整个数组的起始和结束索引入栈
    stack.push(0);
    stack.push(nums.length - 1);

    // 当栈不为空时,进行循环
    while(!stack.isEmpty()) {
        // 从栈中弹出一对索引,表示当前要处理的子数组的起始和结束索引
        int end = stack.pop();
        int start = stack.pop();

        // 对当前子数组进行分区,并获取枢轴元素的索引
        int mid = partition(nums, start, end);

        // 如果枢轴元素左侧的子数组长度大于1,那么将其起始和结束索引入栈
        if(mid - 1 > start) {
            stack.push(start);
            stack.push(mid - 1);
        }

        // 如果枢轴元素右侧的子数组长度大于1,那么将其起始和结束索引入栈
        if(mid + 1 < end) {
            stack.push(mid + 1);
            stack.push(end);
        }
    }
}

// 分区函数跟之前的一样
private static int partition( int[] nums, int start, int end ){
    int pivot = nums[start];
    int left = start; 
    int right = end; 

    while ( left < right ){
        while ( left < right && nums[right] >= pivot )
            right --;
        nums[left] = nums[right];

        while ( left < right && nums[left] <= pivot )
            left ++;
        nums[right] = nums[left];
    }
    nums[left] = pivot;
    return left;
}

在这个实现中,我们使用了一个栈来追踪需要进行排序的子数组的起始和结束索引。初始时,我们把整个数组的起始和结束索引入栈。然后,在循环中,我们每次从栈中弹出一对索引,进行分区操作,并将结果不止一个元素的子数组的起始和结束索引再次入栈。这个过程一直进行,直到栈为空,也就是所有的子数组都已经排序完成。  

3.为什么快速排序的时间复杂度是O(nlogn)

快速排序是一种基于分治策略的排序算法。它的工作原理是选取一个元素(称为"枢轴"或"pivot"),并把数组分为两部分,一部分包含所有小于枢轴的元素,另一部分包含所有大于枢轴的元素。然后,对这两部分递归地进行快速排序。这个过程中,每一次的分区操作(partitioning)都会将枢轴放在其最终排序后的位置。

快速排序的时间复杂度分析需要考虑两个部分:一是划分操作,二是递归调用。

  1. 划分操作:对于一个长度为n的数组,划分操作需要O(n)的时间,因为它需要遍历数组中的每一个元素。

  2. 递归调用:划分操作之后,数组被划分为两部分,假设划分比较均匀,那么这两部分的长度大约为n/2。然后,我们对这两部分分别进行快速排序。这是一个递归过程,其递归深度可以用logn来描述(因为每次我们都将问题的规模减半)。

所以,总的时间复杂度就是O(nlogn),这是因为我们对于每个递归的层级(共logn层)都进行了线性时间复杂度(O(n))的工作。

需要注意的是,这是在最好和平均情况下的时间复杂度。在最坏的情况下(即输入数组已经排序,或者输入数组是逆序排列),快速排序的时间复杂度会退化为O(n²)。这是因为在这种情况下,每次的划分并不会把数组均匀地划分成两部分,而是一边有n-1个元素,另一边只有0或1个元素。但是,这种最坏情况在实际应用中很少发生,特别是当我们选择合适的枢轴(比如使用"三数取中"的策略)时,可以大大减小这种情况的发生概率。

4.快速排序的思想是什么? 

 快速排序是一种非常高效的排序算法,它的基本思想是“分治”。

在快速排序中,我们选择数组中的一个元素作为枢轴(pivot),然后将其他所有元素与这个枢轴进行比较,把数组分为两个部分:一个部分的所有元素都小于枢轴,另一个部分的所有元素都大于枢轴。这个过程被称为分区(partitioning)。枢轴元素在分区后就位于其最终排序后的正确位置。

然后,我们对两个子数组分别重复上述过程:选择一个枢轴,进行分区,然后再递归地对子数组进行快速排序。这就是“分治”的思想:我们将一个大问题分解为几个小问题来解决,然后再将这些小问题的解组合起来,得到大问题的解。

由于我们在每次分区操作后都能确定一个元素的最终位置,所以快速排序通常比其他基于比较的排序算法更快。在最好和平均情况下,快速排序的时间复杂度是O(n log n)。在最坏情况下(即输入数组已经排序或逆序排序),快速排序的时间复杂度是O(n²),但是这种情况在实际应用中很少发生,特别是当我们选择合适的枢轴时。

总的来说,快速排序的思想是通过分区操作,每次确定一个元素的位置,然后递归地对剩余部分进行同样的操作,从而达到整个数组排序的目的。

 5.为什么用栈?

 在这个快速排序的非递归实现中,我们使用栈来保存待排序子数组的起始和结束索引。栈是一种后入先出(LIFO)的数据结构,这意味着最后放入栈的元素会被首先取出。这种特性使栈非常适合用于保存递归调用的状态,因为在递归中,最后进入的递归级别总是首先返回。

在递归版本的快速排序中,当我们对一个子数组进行分区操作并获得枢轴索引后,我们会递归地对左侧和右侧子数组进行快速排序。在非递归版本中,我们将这些子数组的起始和结束索引压入栈中,然后在循环中从栈中弹出这些索引,对相应的子数组进行分区操作,再将新的子数组的索引压入栈中。这个过程会一直进行,直到栈为空,也就是所有的子数组都已经排序完成。

使用栈的好处是我们可以手动控制这个过程,不需要依赖系统的函数调用栈,这样就避免了递归可能导致的栈溢出问题。此外,这样做还使我们能够更好地理解和控制快速排序的过程。

3.3  归并排序

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

归并排序的时间复杂度是O(nlogn)。代价是需要额外的内存空间。

1.实现 

 通过一个简单的数组排序例子来理解归并排序的过程。假设我们有一个包含7个整数的数组[38, 27, 43, 3, 9, 82, 10],我们需要将它按照升序排序。

public class MergeSort {

    void merge(int arr[], int l, int m, int r) {
        // 计算两个子数组的长度
        int n1 = m - l + 1;
        int n2 = r - m;

        // 创建临时数组
        int L[] = new int [n1];
        int R[] = new int [n2];

        // 复制数据到临时数组中
        for (int i=0; i<n1; ++i)
            L[i] = arr[l + i];
        for (int j=0; j<n2; ++j)
            R[j] = arr[m + 1+ j];

        // 合并临时数组

        // 初始化索引
        int i = 0, j = 0;

        int k = l;
        while (i < n1 && j < n2) {
            if (L[i] <= R[j]) {
                arr[k] = L[i];
                i++;
            } else {
                arr[k] = R[j];
                j++;
            }
            k++;
        }

        // 如果任何一个数组还有剩余元素,将其复制到原数组中
        while (i < n1) {
            arr[k] = L[i];
            i++;
            k++;
        }

        while (j < n2) {
            arr[k] = R[j];
            j++;
            k++;
        }
    }

    void sort(int arr[], int l, int r) {
        if (l < r) {
            // 找到中点
            int m = (l+r)/2;

            // 对两个子数组进行排序
            sort(arr, l, m);
            sort(arr , m+1, r);

            // 合并两个子数组
            merge(arr, l, m, r);
        }
    }

    // 测试方法
    public static void main(String args[]) {
        int arr[] = {38, 27, 43, 3, 9, 82, 10};

        System.out.println("给定的数组");
        System.out.println(Arrays.toString(arr));

        MergeSort ob = new MergeSort();
        ob.sort(arr, 0, arr.length-1);

        System.out.println("\n排序后的数组");
        System.out.println(Arrays.toString(arr));
    }
}

 在这个例子中,sort方法是归并排序的主要部分,它首先找到数组的中点,然后递归地对左半部分和右半部分进行排序,最后调用merge方法将两个已排序的半部分合并为一个完整的已排序数组。

merge方法首先计算两个子数组的长度,然后创建两个临时数组并将子数组的元素复制到临时数组中。然后,它通过比较两个临时数组的元素来合并两个子数组,直到一个临时数组的元素都已经被复

2.归并排序的优点是什么?

归并排序(Merge Sort)是一种非常有效的排序算法,具有以下几个主要优点:

  1. 稳定性:归并排序是稳定的排序算法,即相等的元素的相对位置在排序后不会改变。这对于需要保持数据稳定性的场合非常重要。

  2. 时间复杂度:归并排序的时间复杂度为O(n log n),在所有情况下都能保证这个效率,包括最坏情况。这使得归并排序在处理大数据集时非常有效。

  3. 并行化:归并排序算法的结构适合并行化。因为分割步骤是完全独立的,可以在多个处理器或核心上同时进行。

  4. 外部排序:归并排序是一种典型的可以用于外部排序的算法,即数据量大到无法全部装入内存,需要借助外部存储进行排序。归并排序可以很好地处理这种情况,因为它可以分阶段地处理数据,而不需要一次性装入所有数据。

然而,也值得注意的是,归并排序需要与原始数据集同样大小的额外空间来存储临时数据,这在空间复杂度上是一个缺点,特别是对于内存有限的系统。这也是为什么在一些内存敏感的场景中,可能会选择其他排序算法(如快速排序或堆排序)的原因。

3.大数据领域,为什么要用归并排序的外部排序?不是会有temp数组占用空间吗,为什么适合大数据领域? 

 大数据领域中的排序问题常常涉及到的数据量是如此之大,以至于无法全部装入内存中。在这种情况下,我们需要使用一种叫做"外部排序"的技术,其中最常见的就是"外部归并排序"。

外部归并排序的主要思想是将大文件分解成多个小文件,每个小文件的大小都能够装入内存。然后对每个小文件进行排序(这可以使用任何一种有效的内部排序算法,例如快速排序、堆排序或者归并排序等),然后再将排序好的小文件进行归并,最终得到一个完全排序的大文件。

外部归并排序的关键在于归并阶段。在这个阶段,我们需要同时从多个已排序的小文件中读取数据,并且选择最小的元素写入到输出文件。这可以通过一个最小堆来实现,其大小为小文件的数量。这样,每次从堆中取出最小元素并写入到输出文件,然后从该元素所在的文件中读取下一个元素并插入到堆中。这个过程一直持续到所有的文件都被完全读取。

这种方法的优点是,虽然我们需要在内存中维护一个最小堆,但是其大小只取决于同时归并的小文件的数量,而不是数据的总量。因此,即使是非常大的数据集,我们也只需要有限的内存就能够进行排序。

同时,因为归并排序是一种稳定的排序算法,所以它也可以用于处理包含重复键值的数据集,而不会打乱相同键值的原始顺序。

总的来说,外部归并排序因为其对内存使用的有效性,稳定性以及能够处理非常大的数据集的特性,使其成为大数据领域中的一个重要工具。

3.4 遍历二叉树

题目:求下面二叉树的各种遍历(前序、中序、后序、层次)

  1. 中序遍历:即左-根-右遍历,对于给定的二叉树根,寻找其左子树;对于其左子树的根,再去寻找其左子树;递归遍历,直到寻找最左边的节点i,其必然为叶子,然后遍历i的父节点,再遍历i的兄弟节点。随着递归的逐渐出栈,最终完成遍历
  2. 先序遍历:即根-左-右遍历
  3. 后序遍历:即左-右-根遍历
  4. 层序遍历:按照从上到下、从左到右的顺序,逐层遍历所有节点。

 1.层序遍历实现

 在 Java 中,我们通常使用队列来实现二叉树的层序遍历。在这种方法中,我们首先将根节点添加到空队列中。然后,我们执行以下步骤,直到队列为空:

  • 从队列的头部删除一个元素,并输出或处理它。

  • 如果该节点有左子节点,则将左子节点添加到队列的尾部。

  • 如果该节点有右子节点,则将右子节点添加到队列的尾部。

下面是一个简单的层序遍历二叉树的 Java 实现:

import java.util.Queue;
import java.util.LinkedList;

class Node {
    int data;
    Node left, right;
    public Node(int item) {
        data = item;
        left = right = null;
    }
}

class BinaryTree {
    Node root;

    void printLevelOrder() {
        Queue<Node> queue = new LinkedList<Node>();
        queue.add(root);
        while (!queue.isEmpty()) {
            // poll() removes the present head.
            Node tempNode = queue.poll();
            System.out.print(tempNode.data + " ");

            /*Enqueue left child */
            if (tempNode.left != null) {
                queue.add(tempNode.left);
            }

            /*Enqueue right child */
            if (tempNode.right != null) {
                queue.add(tempNode.right);
            }
        }
    }

    public static void main(String args[]) {
        BinaryTree tree_level = new BinaryTree();
        tree_level.root = new Node(1);
        tree_level.root.left = new Node(2);
        tree_level.root.right = new Node(3);
        tree_level.root.left.left = new Node(4);
        tree_level.root.left.right = new Node(5);

        System.out.println("Level order traversal of binary tree is - ");
        tree_level.printLevelOrder();
    }
}

 在这个例子中,printLevelOrder 方法使用队列实现了二叉树的层序遍历。当我们运行程序时,它会输出:1 2 3 4 5,这正是我们期望的层序遍历结果。

2.语言描述

 首先,我们需要创建一个队列(在 Java 中使用 LinkedList 实现),队列是一种先进先出(FIFO)的数据结构,这意味着我们先放入队列的元素会先被取出。

我们首先将二叉树的根节点(在我们的例子中是数字 1 的节点)放入队列。

然后,我们开始一个循环,只要队列中有元素,就继续这个循环。

在循环的每一步中:

  • 我们先从队列中取出(并从队列中移除)一个节点,这就是当前我们要处理的节点,我们将这个节点的值打印出来。在第一次循环中,这个节点就是根节点。

  • 然后,我们查看这个节点是否有左子节点,如果有,我们就将左子节点放入队列的末尾。在第一次循环中,根节点的左子节点(数字 2 的节点)会被放入队列。

  • 接着,我们查看这个节点是否有右子节点,如果有,我们就将右子节点放入队列的末尾。在第一次循环中,根节点的右子节点(数字 3 的节点)也会被放入队列。

完成第一次循环后,我们的队列中有两个元素:数字 2 的节点和数字 3 的节点,它们是根节点的左子节点和右子节点。同时,我们已经打印出了根节点的值 1。

在下一次循环中,我们将处理数字 2 的节点(因为它是最早被放入队列的),并将其左子节点和右子节点(如果有的话)放入队列。然后我们继续下一次循环,处理数字 3 的节点,依此类推。

我们将继续这个循环,直到队列中没有元素。这样,我们就按照层序遍历了整棵二叉树,每个节点都被打印出来了。

3.5 二分查找

给定一个n个元素有序的(升序)整型数组nums和一个目标值target,写一个函数搜索nums中的target,如果目标值存在返回下标,否则返回-1。

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法,前提是数据结构必须先排好序,可以在对数时间复杂度内完成查找。

二分查找事实上采用的就是一种分治策略,它充分利用了元素间的次序关系,可在最坏的情况下用O(log n)完成搜索任务。

 1.实现

/**
 * @param a  要查找的有序int数组
 * @param key  要查找的数值元素
 * @return  返回找到的元素下标;如果没有找到,返回-1
 */
public int binarySearch(int[] a, int key){
    // 初始化 low 和 high 分别指向数组的起始位置和结束位置
    int low = 0;
    int high = a.length - 1;

    // 如果要查找的元素比整个数组的最小元素还小,或者比整个数组的最大元素还大,那么直接返回 -1
    if ( key < a[low] || key > a[high] )
            return -1;

    // 当 low 不大于 high 时,进行循环
    while ( low <= high){
        // 计算中间元素的索引
        int mid = ( low + high ) / 2;

        // 如果中间元素的值小于要查找的值,那么要查找的值应在 mid 右侧,所以将 low 设置为 mid + 1
        if( a[mid] < key)
            low = mid + 1;
        // 如果中间元素的值大于要查找的值,那么要查找的值应在 mid 左侧,所以将 high 设置为 mid - 1
        else if( a[mid] > key )
            high = mid - 1;
        // 如果中间元素的值等于要查找的值,那么已经找到,返回 mid
        else
            return mid; 
    }
    
    // 如果循环结束还没有找到,说明数组中没有要查找的元素,返回 -1
    return -1;
}

2.语言描述

二分查找是一种在有序数组中查找特定元素的搜索算法。在每一步,算法比较数组中间元素和目标值。如果中间元素等于目标值,搜索结束。如果目标值不在中间元素处,算法在大于或小于中间元素的那一半数组中重复搜索过程。

以下是代码的详细步骤:

  1. 初始化两个指针,lowhighlow 指向数组的开始,high 指向数组的结尾。

  2. 首先,检查目标值 key 是否在数组的范围内。如果 key 小于最小值或大于最大值,那么返回 -1,表示 key 不在数组中。

  3. 进入一个循环,只要 low 不大于 high,就继续这个循环。在循环的每一次迭代中:

    • 计算中间索引 mid,这是 lowhigh 的平均值(向下取整)。

    • 检查 mid 索引处的元素。如果它小于 key,说明 key 一定在 mid 之后的那部分数组中,所以将 low 更新为 mid + 1

    • 如果 mid 索引处的元素大于 key,说明 key 一定在 mid 之前的那部分数组中,所以将 high 更新为 mid - 1

    • 如果 mid 索引处的元素等于 key,那么我们找到了目标,返回 mid,这就是 key 在数组中的位置。

  4. 如果循环结束,我们还没有找到 key,那么返回 -1,表示 key 不在数组中。

3.6 小青蛙跳台阶

题目:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙上一个n级台阶总共有多少种跳法?

1.实现

小青蛙跳台阶 题目:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙上一个n级台阶总共有多少种跳法?

这个问题其实是一个动态规划问题,也是斐波那契数列的一个应用。

假设我们要跳上n级台阶,那么我们最后一步有两种选择:

  1. 从n-1级台阶直接跳上来,有f(n-1)种方法(f(n)表示跳上n级台阶的跳法数量)

  2. 从n-2级台阶直接跳上来,有f(n-2)种方法。

因此,跳上n级台阶的总的跳法数量f(n)就是这两种情况的和,即f(n) = f(n-1) + f(n-2)。

这就是斐波那契数列的递推公式,所以我们可以用斐波那契数列的方法解决这个问题。

以下是使用Java实现的代码:

public int climbStairs(int n) {
    // 如果 n 小于等于2,直接返回 n。因为1级台阶有1种跳法,2级台阶有2种跳法
    if (n <= 2) {
        return n;
    }

    // 初始化前两个数的值,pre2 代表 f(n-2),pre1 代表 f(n-1)
    int pre2 = 1, pre1 = 2;

    // 从3开始遍历,因为1和2的情况已经在前面处理了
    for (int i = 3; i <= n; i++) {
        // 当前的跳法数量等于前两个数的和
        int curr = pre1 + pre2;

        // 更新 pre2 和 pre1 的值,为下一次循环做准备
        pre2 = pre1;
        pre1 = curr;
    }

    // 返回 f(n),即 n 级台阶的跳法数量
    return pre1;
}

在这段代码中,我们首先处理了n小于等于2的特殊情况。然后,我们使用一个循环来计算f(n)。在每一次迭代中,我们计算出f(i),然后更新pre2和pre1的值以供下一次迭代使用。最后,我们返回f(n)。

2.理解

只要跳上去,就算一种跳法

3.例子

我们可以按照跳跃的顺序来考虑。如果有4级台阶,那么有5种跳法:

1.跳4次1级台阶(1-1-1-1) 2.跳1次2级台阶,再跳2次1级台阶(2-1-1) 3.跳2次1级台阶,再跳1次2级台阶(1-1-2) 4.跳2次2级台阶(2-2) 5.跳1次1级台阶,再跳1次2级台阶,再跳1次1级台阶(1-2-1)

这里考虑了所有可能的跳法,不同的跳法之间的区别在于1级台阶和2级台阶的跳跃顺序。

 4.语言描述

首先,我们需要理解这个问题:一只青蛙要跳上n级台阶,它每次可以跳1级,也可以跳2级。我们需要找出跳上这n级台阶的所有可能的跳法。

  1. 如果只有1级台阶或者2级台阶,那么青蛙的跳法分别就是1种和2种,因为对于1级台阶,青蛙只能一次跳上去,对于2级台阶,青蛙可以一次跳2级,或者先跳1级再跳1级。所以,如果n小于等于2,我们直接返回n即可。

    1.     if (n <= 2) {
              return n;
          }

  2.  如果有超过2级台阶,那么我们可以用一个循环来计算。假设我们已经知道跳上n-1级台阶的跳法数量为pre1,跳上n-2级台阶的跳法数量为pre2。那么跳上n级台阶的跳法数量就是pre1和pre2的和。因为青蛙可以选择从n-1级台阶一步跳上来,也可以选择从n-2级台阶一步跳上来。
    1.     int pre2 = 1, pre1 = 2;
          for (int i = 3; i <= n; i++) {
              int curr = pre1 + pre2;
              pre2 = pre1;
              pre1 = curr;
          }

  3.  最后,当循环结束时,我们就得到了跳上n级台阶的跳法数量,即pre1。
    1. return pre1;

通过以上步骤,我们可以计算出青蛙跳上任意级台阶的跳法数量。这种方法的优点是我们只需要保存最近两个数的跳法数量,这样就大大减少了计算和存储的复杂性。

 5.斐波那契数列是什么

斐波那契数列是一个非常著名的数列,由意大利数学家莱昂纳多·斐波那契在1202年的《算盘书》中引入。

斐波那契数列的定义如下:

  • 第一项和第二项都是1。

  • 从第三项开始,每一项都等于前两项之和。

也就是说,数列的前几项为:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ……,并且以此类推。

斐波那契数列有很多有趣的性质和广泛的应用,包括在计算机科学、数学和自然科学中。例如,斐波那契数列在计算机科学中常常被用于算法设计,特别是动态规划和递归问题中。

6.如果加上条件:青蛙还可以1次跳3个台阶,怎么算?    

如果青蛙可以一次跳1级台阶、2级台阶或者3级台阶,那么跳到n级台阶的跳法数f(n)将变为 f(n-1) + f(n-2) + f(n-3),这是因为:

  1. 如果青蛙在最后一步跳1级台阶到达n级台阶,那么之前他需要跳到n-1级台阶,有f(n-1)种方法;

  2. 如果青蛙在最后一步跳2级台阶到达n级台阶,那么之前他需要跳到n-2级台阶,有f(n-2)种方法;

  3. 如果青蛙在最后一步跳3级台阶到达n级台阶,那么之前他需要跳到n-3级台阶,有f(n-3)种方法。

因此,青蛙跳到n级台阶的跳法数为这三种情况的和。

Java代码如下:

public int jump(int n) {
    if (n <= 0) {
        return 0;
    }
    if (n <= 2) {
        return n;
    }
    if (n == 3) {
        return 4;
    }

    int pre3 = 1, pre2 = 2, pre1 = 4;
    int result = 0;

    for (int i = 4; i <= n; i++) {
        result = pre3 + pre2 + pre1;
        pre3 = pre2;
        pre2 = pre1;
        pre1 = result;
    }

    return result;
}

在这段代码中,我们使用了动态规划的思想,从下往上计算跳法数。首先,我们知道跳到1级台阶有1种方法,跳到2级台阶有2种方法,跳到3级台阶有4种方法。然后,我们用一个for循环,从第4级台阶开始计算,每次计算出跳到当前级台阶的方法数,并更新pre3、pre2、pre1的值,直到计算出跳到n级台阶的方法数。最后,返回这个结果。

6.1 如果台阶n为3,那么有几种情况,分别列出来

如果青蛙一次可以跳上1级台阶,也可以跳上2级台阶,还可以跳上3级台阶,那么跳上一个3级台阶总共有4种跳法:

  1. 直接一次跳上3级台阶

  2. 先跳1级台阶,再跳2级台阶

  3. 先跳2级台阶,再跳1级台阶

  4. 连续跳3次1级台阶

所以,如果台阶为3,总共有4种跳法。

3.7  最长回文子串

题目:给你一个字符串s,找到s中最长的回文子串。

实例:

输入:s = “babad”

输出:“bab”

解释:“aba”也是符合题意答案

2.实现

public class Solution {
    public String longestPalindrome(String s) {
        // 空字符串直接返回""
        if (s == null || s.length() == 0) {
            return "";
        }
        int length = s.length();
        // 定义二维数组,isPalindrome[i][j]表示s从i到j的子字符串是否为回文
        boolean[][] isPalindrome = new boolean[length][length];
        // 定义最长回文子串的起始和终止位置
        int left = 0;
        int right = 0;

        // 外层循环遍历字符串的末端
        for (int j = 1; j < length; j++) {
            // 内层循环遍历字符串的起始位置
            for (int i = 0; i < j; i++) {
                // 判断子字符串的内部是否为回文,或者子字符串长度小于等于2
                boolean isInnerWordPalindrome = isPalindrome[i + 1][j - 1] || j - i <= 2;
                // 如果子字符串的两端字符相同且内部为回文,则该子字符串为回文
                if (s.charAt(i) == s.charAt(j) && isInnerWordPalindrome) {
                    isPalindrome[i][j] = true;
                    // 如果找到的回文子串比先前找到的更长,则更新最长回文子串的起始和终止位置
                    if (j - i > right - left) {
                        left = i;
                        right = j;
                    }
                }
            }
        }
        // 返回最长回文子串
        return s.substring(left, right + 1);
    }
}

这段代码的工作原理是通过动态规划的方法来检查字符串中的所有子字符串,看它们是否是回文。首先,我们初始化一个二维布尔数组,然后我们遍历字符串中的每一个字符,如果当前字符与另一个字符相同,并且这两个字符之间的子字符串也是回文(或者这两个字符就是相邻的,没有中间的子字符串),那么我们就找到了一个新的回文子字符串。如果这个新的回文子字符串比之前找到的回文子字符串还要长,那么我们就更新最长的回文子字符串。最后,我们返回最长的回文子字符串。

3.举例说明上面的代码是怎么运行的

假设我们的输入字符串是 "babad"。

在第一个外部循环迭代中,我们检查所有以第二个字符结尾的子字符串:

  1. "ba" 不是回文,所以 isPalindrome0 = false。

  2. "bab" 是回文,因为 "a" 是回文且 'b' == 'b',所以 isPalindrome0 = true,更新最长回文子串为 "bab"。

  3. "baba" 不是回文,因为 "ab" 不是回文,所以 isPalindrome0 = false。

  4. "babad" 不是回文,因为 "aba" 是回文但 'd' != 'b',所以 isPalindrome0 = false。

在第二个外部循环迭代中,我们检查所有以第三个字符结尾的子字符串:

  1. "ab" 不是回文,所以 isPalindrome1 = false。

  2. "aba" 是回文,因为 ""(空字符串)是回文且 'a' == 'a',所以 isPalindrome1 = true,但不更新最长回文子串,因为当前长度等于已知的最长长度。

  3. "abad" 不是回文,因为 "ba" 不是回文,所以 isPalindrome1 = false。

我们继续这个过程,直到遍历完所有以每个字符结尾的子字符串。在这个例子中,最后我们找到的最长回文子串是 "bab"。

这个过程的关键在于,我们只查看以每个字符结尾的子字符串,然后检查它们是否为回文。我们使用了动态规划的思想,即如果我们已经知道一个子字符串是回文,那么只需要比较它两端的字符就可以确定一个更长的子字符串是否也是回文。我们使用二维数组存储已经计算过的子字符串是否是回文的结果,这样可以避免重复计算。

4.什么是回文

回文是一种特殊的字符串,它从前往后读和从后往前读是完全一样的。也就是说,回文是左右对称的。例如,"madam","12321","racecar",和"level"都是回文,因为它们从前向后和从后向前读起来都是一样的。

回文也可以是一个句子,比如 "Able was I ere I saw Elba"。忽略大小写和空格后,这个句子从前向后和从后向前读都是一样的。

在编程中,判断一个字符串是否是回文常常是一个常见的问题。这通常可以通过双指针或者递归的方式来实现。一般的做法是比较字符串的第一个字符和最后一个字符,然后比较第二个字符和倒数第二个字符,以此类推,直到字符串的中间部分。如果在这个过程中所有的字符都是匹配的,那么这个字符串就是回文。

4.1 ab和ba是回文吗?

不,"ab" 和 "ba" 不是回文。回文字符串是指从前往后读和从后往前读都是完全相同的字符串。

比如,"ab" 从前往后读是 "ab",从后往前读则是 "ba",因此 "ab" 不是回文。

同样地,"ba" 从前往后读是 "ba",从后往前读则是 "ab",因此 "ba" 也不是回文。

所以 "ab" 和 "ba" 都不是回文字符串。

 4.2 aa是回文吗?

是的,"aa" 是一个回文字符串。回文字符串的定义是这样的:从前往后读和从后往前读都是完全相同的字符串。

在 "aa" 这个例子中,无论你从哪个方向读,都是 "aa",所以 "aa" 是一个回文字符串。

 4.3 a算回文吗?

是的,单个字符(例如 'a')被视为回文。这是因为回文的定义是正向和反向读取都一样的字符序列。因此,单个字符自然满足这个定义,因为它只有一个字符,无论从哪个方向读取都是一样的。

 5.boolean类型的二维数组怎么使用?举个例子

在Java中,可以使用二维boolean数组来存储二维的真/假值。这种二维数组实际上是数组的数组。每个元素都是一个boolean数组。这种结构允许我们根据两个索引来访问、存储和修改数据。

下面是一个简单的例子,它创建了一个5x5的二维boolean数组,并在其中填充了一些值:

public class Main {
    public static void main(String[] args) {
        // 创建一个5x5的二维boolean数组
        boolean[][] matrix = new boolean[5][5];

        // 填充一些值
        matrix[0][0] = true;
        matrix[2][2] = true;
        matrix[4][4] = true;

        // 打印数组内容
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[i].length; j++) {
                System.out.print(matrix[i][j] ? "T " : "F ");
            }
            System.out.println();
        }
    }
}

 运行上述代码,你将看到如下输出:

T F F F F 
F F F F F 
F F T F F 
F F F F F 
F F F F T 

在这个例子中,我们创建了一个二维数组matrix,然后我们在特定的位置设定了值为true,并打印了数组的所有元素。请注意,如果你不明确地为Java中的boolean数组元素设定值,它们会默认初始化为false

6.为什么用二维数组保存?

在最长回文子串问题中,使用二维boolean数组 isPalindrome 是一种常用的动态规划方法。数组 isPalindrome[i][j] 的含义是子串 s[i]...s[j] 是否为回文。

理由如下:

  1. 节省计算时间:在处理字符串的过程中,我们可能会多次检查相同的子串是否为回文。如果我们每次都重新计算,那么就会浪费很多时间。通过在二维数组中存储结果,我们可以避免重复计算。

  2. 便于回溯:当我们在字符串中找到一个回文子串时,我们可能需要回到它的前一部分继续查找更长的回文子串。这时,我们可以直接在二维数组中查找结果,而不需要重新计算。

  3. 动态规划:这是一个动态规划问题,我们需要保存之前的计算结果,以便进行后续的计算。动态规划是一种通过将复杂问题分解成更简单的子问题,然后将子问题的结果存储起来,从而避免重复计算子问题的方法。

通过使用这种二维布尔数组,我们可以在 O(n^2) 的时间复杂度内解决这个问题,其中 n 是字符串的长度。如果我们不使用这种方法,而是直接计算每一个可能的子串,那么时间复杂度将会增加到 O(n^3)

7.什么是动态规划

动态规划是一种用于解决多阶段决策问题的优化算法。它将一个复杂问题分解为一系列更简单的子问题(也称为状态),并存储这些子问题的结果,以便在解决更大问题时进行查找(也称为查表),从而避免了重复解决相同子问题带来的计算开销。这种技术称为"记忆化"。

以下是动态规划的主要特点:

  1. 重叠子问题:在计算过程中,一些问题可能会被多次计算。例如,在斐波那契数列问题中,我们可能会多次计算fib(n-2)。在这种情况下,我们可以将这个子问题的结果存储起来,以供后续使用。

  2. 最优子结构:原问题的最优解可以通过其子问题的最优解推导出来。这意味着我们可以通过解决子问题并将子问题的解决方案组合起来来找到原问题的最优解。

  3. 状态转移方程:每个状态都可以通过其他状态推导出来,状态之间的关系可以用方程或者递推关系来表示。

  4. 边界问题:最小的子问题可以直接解决,不需要进一步分解。

例如,动态规划常用于求解最长公共子序列、最短路径问题、背包问题等。

简单来说,动态规划的思想就是大事化小,小事化了,通过解决每一个小问题,最终解决整个大问题。

3.8 数字字符转化成IP

题目:现在有一个只包含数字的字符串,将该字符串转化成IP地址的形式,返回所有可能的情况。

例如:

给出的字符串为“25525511135”

返回["255.255.11.135", "255.255.111.35"](顺序没有关系)

 2.实现

package com.atguigu;

import java.util.ArrayList;
import java.util.List;

public class IPAddressConverter {
    public List<String> convertToIP(String digits) {
        List<String> result = new ArrayList<>();

        // 检查边界情况,字符串为空或长度小于4或大于12时无法组成合法的IP地址
        if (digits == null || digits.length() < 4 || digits.length() > 12) {
            return result;
        }

        backtrack(result, digits, new StringBuilder(), 0, 0);
        return result;
    }

    private void backtrack(List<String> result, String digits, StringBuilder ipAddress, int segmentCount, int index) {

        if (segmentCount == 4 && index != digits.length()) {
            System.out.println("不满足条件,递归返回");
            return;
        }

        // 如果已经有4个段并且遍历完所有数字,表示找到了一种合法的IP地址形式,将其添加到结果中
        if (segmentCount == 4) {
            result.add(ipAddress.toString());
            return;
        }

        // 每个段可以有1到3个数字
        for (int i = 1; i <= 3; i++) {
            // 如果当前索引超出了字符串长度,结束循环
            if (index + i > digits.length()) {
                break;
            }

            // 截取当前段的字符串
            String segment = digits.substring(index, index + i);

            System.out.println("截取的segment为:" + segment);

            // 排除以0开头的多位数字
            if (segment.startsWith("0") && segment.length() > 1) {
                System.out.println("排除以0开头的多位数字");
                continue;
            }

            // 将段转换为数字
            int num = Integer.parseInt(segment);
            // 检查数字是否在合法的范围内
            if (num >= 0 && num <= 255) {
                // 如果不是第一个段,在段之间添加一个点
                if (segmentCount > 0) {
                    ipAddress.append(".");
                }
                // 添加当前段到IP地址中
                System.out.println(segment);
                ipAddress.append(segment);
                System.out.println("ipAddress:" + ipAddress);
                // 递归处理下一个段

                System.out.println("开始递归调用backtrack()方法,index为:" + index);
                backtrack(result, digits, ipAddress, segmentCount + 1, index + i);

                // 回溯时将添加的当前段从IP地址中删除
                System.out.println("删除:" + ipAddress);
                System.out.println("i为:" + i + "  segmentCount为:" + segmentCount);

                System.out.println("删除的起始位置为:" + (ipAddress.length() - i - (segmentCount > 0 ? 1 : 0)));
                System.out.println("删除的结束位置为:" + ipAddress.length());

                ipAddress.delete(ipAddress.length() - i - (segmentCount > 0 ? 1 : 0), ipAddress.length());

                System.out.println("删除后:" + ipAddress);
            }else {
                System.out.println("数字不在合法的范围内:" + num);
            }
        }
    }

    public static void main(String[] args) {
        IPAddressConverter converter = new IPAddressConverter();
        List<String> ipAddresses = converter.convertToIP("25525511135");
        System.out.println(ipAddresses);
    }
}

3.结果

C:\jdk\bin\java.exe -javaagent:D:\IntelliJIDEA2022.3.1\lib\idea_rt.jar=6805:D:\IntelliJIDEA2022.3.1\bin -Dfile.encoding=UTF-8 -classpath C:\jdk\jre\lib\charsets.jar;C:\jdk\jre\lib\deploy.jar;C:\jdk\jre\lib\ext\access-bridge-64.jar;C:\jdk\jre\lib\ext\cldrdata.jar;C:\jdk\jre\lib\ext\dnsns.jar;C:\jdk\jre\lib\ext\jaccess.jar;C:\jdk\jre\lib\ext\jfxrt.jar;C:\jdk\jre\lib\ext\localedata.jar;C:\jdk\jre\lib\ext\nashorn.jar;C:\jdk\jre\lib\ext\sunec.jar;C:\jdk\jre\lib\ext\sunjce_provider.jar;C:\jdk\jre\lib\ext\sunmscapi.jar;C:\jdk\jre\lib\ext\sunpkcs11.jar;C:\jdk\jre\lib\ext\zipfs.jar;C:\jdk\jre\lib\javaws.jar;C:\jdk\jre\lib\jce.jar;C:\jdk\jre\lib\jfr.jar;C:\jdk\jre\lib\jfxswt.jar;C:\jdk\jre\lib\jsse.jar;C:\jdk\jre\lib\management-agent.jar;C:\jdk\jre\lib\plugin.jar;C:\jdk\jre\lib\resources.jar;C:\jdk\jre\lib\rt.jar;D:\workspace\shangguigu-xuexi\AlgorithmPractice\target\classes com.atguigu.IPAddressConverter
截取的segment为:2
2
ipAddress:2
开始递归调用backtrack()方法,index为:0
截取的segment为:5
5
ipAddress:2.5
开始递归调用backtrack()方法,index为:1
截取的segment为:5
5
ipAddress:2.5.5
开始递归调用backtrack()方法,index为:2
截取的segment为:2
2
ipAddress:2.5.5.2
开始递归调用backtrack()方法,index为:3
不满足条件,递归返回
删除:2.5.5.2
i为:1  segmentCount为:3
删除的起始位置为:5
删除的结束位置为:7
删除后:2.5.5
截取的segment为:25
25
ipAddress:2.5.5.25
开始递归调用backtrack()方法,index为:3
不满足条件,递归返回
删除:2.5.5.25
i为:2  segmentCount为:3
删除的起始位置为:5
删除的结束位置为:8
删除后:2.5.5
截取的segment为:255
255
ipAddress:2.5.5.255
开始递归调用backtrack()方法,index为:3
不满足条件,递归返回
删除:2.5.5.255
i为:3  segmentCount为:3
删除的起始位置为:5
删除的结束位置为:9
删除后:2.5.5
删除:2.5.5
i为:1  segmentCount为:2
删除的起始位置为:3
删除的结束位置为:5
删除后:2.5
截取的segment为:52
52
ipAddress:2.5.52
开始递归调用backtrack()方法,index为:2
截取的segment为:5
5
ipAddress:2.5.52.5
开始递归调用backtrack()方法,index为:4
不满足条件,递归返回
删除:2.5.52.5
i为:1  segmentCount为:3
删除的起始位置为:6
删除的结束位置为:8
删除后:2.5.52
截取的segment为:55
55
ipAddress:2.5.52.55
开始递归调用backtrack()方法,index为:4
不满足条件,递归返回
删除:2.5.52.55
i为:2  segmentCount为:3
删除的起始位置为:6
删除的结束位置为:9
删除后:2.5.52
截取的segment为:551
数字不在合法的范围内:551
删除:2.5.52
i为:2  segmentCount为:2
删除的起始位置为:3
删除的结束位置为:6
删除后:2.5
截取的segment为:525
数字不在合法的范围内:525
删除:2.5
i为:1  segmentCount为:1
删除的起始位置为:1
删除的结束位置为:3
删除后:2
截取的segment为:55
55
ipAddress:2.55
开始递归调用backtrack()方法,index为:1
截取的segment为:2
2
ipAddress:2.55.2
开始递归调用backtrack()方法,index为:3
截取的segment为:5
5
ipAddress:2.55.2.5
开始递归调用backtrack()方法,index为:4
不满足条件,递归返回
删除:2.55.2.5
i为:1  segmentCount为:3
删除的起始位置为:6
删除的结束位置为:8
删除后:2.55.2
截取的segment为:55
55
ipAddress:2.55.2.55
开始递归调用backtrack()方法,index为:4
不满足条件,递归返回
删除:2.55.2.55
i为:2  segmentCount为:3
删除的起始位置为:6
删除的结束位置为:9
删除后:2.55.2
截取的segment为:551
数字不在合法的范围内:551
删除:2.55.2
i为:1  segmentCount为:2
删除的起始位置为:4
删除的结束位置为:6
删除后:2.55
截取的segment为:25
25
ipAddress:2.55.25
开始递归调用backtrack()方法,index为:3
截取的segment为:5
5
ipAddress:2.55.25.5
开始递归调用backtrack()方法,index为:5
不满足条件,递归返回
删除:2.55.25.5
i为:1  segmentCount为:3
删除的起始位置为:7
删除的结束位置为:9
删除后:2.55.25
截取的segment为:51
51
ipAddress:2.55.25.51
开始递归调用backtrack()方法,index为:5
不满足条件,递归返回
删除:2.55.25.51
i为:2  segmentCount为:3
删除的起始位置为:7
删除的结束位置为:10
删除后:2.55.25
截取的segment为:511
数字不在合法的范围内:511
删除:2.55.25
i为:2  segmentCount为:2
删除的起始位置为:4
删除的结束位置为:7
删除后:2.55
截取的segment为:255
255
ipAddress:2.55.255
开始递归调用backtrack()方法,index为:3
截取的segment为:1
1
ipAddress:2.55.255.1
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:2.55.255.1
i为:1  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:10
删除后:2.55.255
截取的segment为:11
11
ipAddress:2.55.255.11
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:2.55.255.11
i为:2  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:11
删除后:2.55.255
截取的segment为:111
111
ipAddress:2.55.255.111
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:2.55.255.111
i为:3  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:12
删除后:2.55.255
删除:2.55.255
i为:3  segmentCount为:2
删除的起始位置为:4
删除的结束位置为:8
删除后:2.55
删除:2.55
i为:2  segmentCount为:1
删除的起始位置为:1
删除的结束位置为:4
删除后:2
截取的segment为:552
数字不在合法的范围内:552
删除:2
i为:1  segmentCount为:0
删除的起始位置为:0
删除的结束位置为:1
删除后:
截取的segment为:25
25
ipAddress:25
开始递归调用backtrack()方法,index为:0
截取的segment为:5
5
ipAddress:25.5
开始递归调用backtrack()方法,index为:2
截取的segment为:2
2
ipAddress:25.5.2
开始递归调用backtrack()方法,index为:3
截取的segment为:5
5
ipAddress:25.5.2.5
开始递归调用backtrack()方法,index为:4
不满足条件,递归返回
删除:25.5.2.5
i为:1  segmentCount为:3
删除的起始位置为:6
删除的结束位置为:8
删除后:25.5.2
截取的segment为:55
55
ipAddress:25.5.2.55
开始递归调用backtrack()方法,index为:4
不满足条件,递归返回
删除:25.5.2.55
i为:2  segmentCount为:3
删除的起始位置为:6
删除的结束位置为:9
删除后:25.5.2
截取的segment为:551
数字不在合法的范围内:551
删除:25.5.2
i为:1  segmentCount为:2
删除的起始位置为:4
删除的结束位置为:6
删除后:25.5
截取的segment为:25
25
ipAddress:25.5.25
开始递归调用backtrack()方法,index为:3
截取的segment为:5
5
ipAddress:25.5.25.5
开始递归调用backtrack()方法,index为:5
不满足条件,递归返回
删除:25.5.25.5
i为:1  segmentCount为:3
删除的起始位置为:7
删除的结束位置为:9
删除后:25.5.25
截取的segment为:51
51
ipAddress:25.5.25.51
开始递归调用backtrack()方法,index为:5
不满足条件,递归返回
删除:25.5.25.51
i为:2  segmentCount为:3
删除的起始位置为:7
删除的结束位置为:10
删除后:25.5.25
截取的segment为:511
数字不在合法的范围内:511
删除:25.5.25
i为:2  segmentCount为:2
删除的起始位置为:4
删除的结束位置为:7
删除后:25.5
截取的segment为:255
255
ipAddress:25.5.255
开始递归调用backtrack()方法,index为:3
截取的segment为:1
1
ipAddress:25.5.255.1
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:25.5.255.1
i为:1  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:10
删除后:25.5.255
截取的segment为:11
11
ipAddress:25.5.255.11
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:25.5.255.11
i为:2  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:11
删除后:25.5.255
截取的segment为:111
111
ipAddress:25.5.255.111
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:25.5.255.111
i为:3  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:12
删除后:25.5.255
删除:25.5.255
i为:3  segmentCount为:2
删除的起始位置为:4
删除的结束位置为:8
删除后:25.5
删除:25.5
i为:1  segmentCount为:1
删除的起始位置为:2
删除的结束位置为:4
删除后:25
截取的segment为:52
52
ipAddress:25.52
开始递归调用backtrack()方法,index为:2
截取的segment为:5
5
ipAddress:25.52.5
开始递归调用backtrack()方法,index为:4
截取的segment为:5
5
ipAddress:25.52.5.5
开始递归调用backtrack()方法,index为:5
不满足条件,递归返回
删除:25.52.5.5
i为:1  segmentCount为:3
删除的起始位置为:7
删除的结束位置为:9
删除后:25.52.5
截取的segment为:51
51
ipAddress:25.52.5.51
开始递归调用backtrack()方法,index为:5
不满足条件,递归返回
删除:25.52.5.51
i为:2  segmentCount为:3
删除的起始位置为:7
删除的结束位置为:10
删除后:25.52.5
截取的segment为:511
数字不在合法的范围内:511
删除:25.52.5
i为:1  segmentCount为:2
删除的起始位置为:5
删除的结束位置为:7
删除后:25.52
截取的segment为:55
55
ipAddress:25.52.55
开始递归调用backtrack()方法,index为:4
截取的segment为:1
1
ipAddress:25.52.55.1
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:25.52.55.1
i为:1  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:10
删除后:25.52.55
截取的segment为:11
11
ipAddress:25.52.55.11
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:25.52.55.11
i为:2  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:11
删除后:25.52.55
截取的segment为:111
111
ipAddress:25.52.55.111
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:25.52.55.111
i为:3  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:12
删除后:25.52.55
删除:25.52.55
i为:2  segmentCount为:2
删除的起始位置为:5
删除的结束位置为:8
删除后:25.52
截取的segment为:551
数字不在合法的范围内:551
删除:25.52
i为:2  segmentCount为:1
删除的起始位置为:2
删除的结束位置为:5
删除后:25
截取的segment为:525
数字不在合法的范围内:525
删除:25
i为:2  segmentCount为:0
删除的起始位置为:0
删除的结束位置为:2
删除后:
截取的segment为:255
255
ipAddress:255
开始递归调用backtrack()方法,index为:0
截取的segment为:2
2
ipAddress:255.2
开始递归调用backtrack()方法,index为:3
截取的segment为:5
5
ipAddress:255.2.5
开始递归调用backtrack()方法,index为:4
截取的segment为:5
5
ipAddress:255.2.5.5
开始递归调用backtrack()方法,index为:5
不满足条件,递归返回
删除:255.2.5.5
i为:1  segmentCount为:3
删除的起始位置为:7
删除的结束位置为:9
删除后:255.2.5
截取的segment为:51
51
ipAddress:255.2.5.51
开始递归调用backtrack()方法,index为:5
不满足条件,递归返回
删除:255.2.5.51
i为:2  segmentCount为:3
删除的起始位置为:7
删除的结束位置为:10
删除后:255.2.5
截取的segment为:511
数字不在合法的范围内:511
删除:255.2.5
i为:1  segmentCount为:2
删除的起始位置为:5
删除的结束位置为:7
删除后:255.2
截取的segment为:55
55
ipAddress:255.2.55
开始递归调用backtrack()方法,index为:4
截取的segment为:1
1
ipAddress:255.2.55.1
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:255.2.55.1
i为:1  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:10
删除后:255.2.55
截取的segment为:11
11
ipAddress:255.2.55.11
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:255.2.55.11
i为:2  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:11
删除后:255.2.55
截取的segment为:111
111
ipAddress:255.2.55.111
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:255.2.55.111
i为:3  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:12
删除后:255.2.55
删除:255.2.55
i为:2  segmentCount为:2
删除的起始位置为:5
删除的结束位置为:8
删除后:255.2
截取的segment为:551
数字不在合法的范围内:551
删除:255.2
i为:1  segmentCount为:1
删除的起始位置为:3
删除的结束位置为:5
删除后:255
截取的segment为:25
25
ipAddress:255.25
开始递归调用backtrack()方法,index为:3
截取的segment为:5
5
ipAddress:255.25.5
开始递归调用backtrack()方法,index为:5
截取的segment为:1
1
ipAddress:255.25.5.1
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:255.25.5.1
i为:1  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:10
删除后:255.25.5
截取的segment为:11
11
ipAddress:255.25.5.11
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:255.25.5.11
i为:2  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:11
删除后:255.25.5
截取的segment为:111
111
ipAddress:255.25.5.111
开始递归调用backtrack()方法,index为:6
不满足条件,递归返回
删除:255.25.5.111
i为:3  segmentCount为:3
删除的起始位置为:8
删除的结束位置为:12
删除后:255.25.5
删除:255.25.5
i为:1  segmentCount为:2
删除的起始位置为:6
删除的结束位置为:8
删除后:255.25
截取的segment为:51
51
ipAddress:255.25.51
开始递归调用backtrack()方法,index为:5
截取的segment为:1
1
ipAddress:255.25.51.1
开始递归调用backtrack()方法,index为:7
不满足条件,递归返回
删除:255.25.51.1
i为:1  segmentCount为:3
删除的起始位置为:9
删除的结束位置为:11
删除后:255.25.51
截取的segment为:11
11
ipAddress:255.25.51.11
开始递归调用backtrack()方法,index为:7
不满足条件,递归返回
删除:255.25.51.11
i为:2  segmentCount为:3
删除的起始位置为:9
删除的结束位置为:12
删除后:255.25.51
截取的segment为:113
113
ipAddress:255.25.51.113
开始递归调用backtrack()方法,index为:7
不满足条件,递归返回
删除:255.25.51.113
i为:3  segmentCount为:3
删除的起始位置为:9
删除的结束位置为:13
删除后:255.25.51
删除:255.25.51
i为:2  segmentCount为:2
删除的起始位置为:6
删除的结束位置为:9
删除后:255.25
截取的segment为:511
数字不在合法的范围内:511
删除:255.25
i为:2  segmentCount为:1
删除的起始位置为:3
删除的结束位置为:6
删除后:255
截取的segment为:255
255
ipAddress:255.255
开始递归调用backtrack()方法,index为:3
截取的segment为:1
1
ipAddress:255.255.1
开始递归调用backtrack()方法,index为:6
截取的segment为:1
1
ipAddress:255.255.1.1
开始递归调用backtrack()方法,index为:7
不满足条件,递归返回
删除:255.255.1.1
i为:1  segmentCount为:3
删除的起始位置为:9
删除的结束位置为:11
删除后:255.255.1
截取的segment为:11
11
ipAddress:255.255.1.11
开始递归调用backtrack()方法,index为:7
不满足条件,递归返回
删除:255.255.1.11
i为:2  segmentCount为:3
删除的起始位置为:9
删除的结束位置为:12
删除后:255.255.1
截取的segment为:113
113
ipAddress:255.255.1.113
开始递归调用backtrack()方法,index为:7
不满足条件,递归返回
删除:255.255.1.113
i为:3  segmentCount为:3
删除的起始位置为:9
删除的结束位置为:13
删除后:255.255.1
删除:255.255.1
i为:1  segmentCount为:2
删除的起始位置为:7
删除的结束位置为:9
删除后:255.255
截取的segment为:11
11
ipAddress:255.255.11
开始递归调用backtrack()方法,index为:6
截取的segment为:1
1
ipAddress:255.255.11.1
开始递归调用backtrack()方法,index为:8
不满足条件,递归返回
删除:255.255.11.1
i为:1  segmentCount为:3
删除的起始位置为:10
删除的结束位置为:12
删除后:255.255.11
截取的segment为:13
13
ipAddress:255.255.11.13
开始递归调用backtrack()方法,index为:8
不满足条件,递归返回
删除:255.255.11.13
i为:2  segmentCount为:3
删除的起始位置为:10
删除的结束位置为:13
删除后:255.255.11
截取的segment为:135
135
ipAddress:255.255.11.135
开始递归调用backtrack()方法,index为:8
删除:255.255.11.135
i为:3  segmentCount为:3
删除的起始位置为:10
删除的结束位置为:14
删除后:255.255.11
删除:255.255.11
i为:2  segmentCount为:2
删除的起始位置为:7
删除的结束位置为:10
删除后:255.255
截取的segment为:111
111
ipAddress:255.255.111
开始递归调用backtrack()方法,index为:6
截取的segment为:3
3
ipAddress:255.255.111.3
开始递归调用backtrack()方法,index为:9
不满足条件,递归返回
删除:255.255.111.3
i为:1  segmentCount为:3
删除的起始位置为:11
删除的结束位置为:13
删除后:255.255.111
截取的segment为:35
35
ipAddress:255.255.111.35
开始递归调用backtrack()方法,index为:9
删除:255.255.111.35
i为:2  segmentCount为:3
删除的起始位置为:11
删除的结束位置为:14
删除后:255.255.111
删除:255.255.111
i为:3  segmentCount为:2
删除的起始位置为:7
删除的结束位置为:11
删除后:255.255
删除:255.255
i为:3  segmentCount为:1
删除的起始位置为:3
删除的结束位置为:7
删除后:255
删除:255
i为:3  segmentCount为:0
删除的起始位置为:0
删除的结束位置为:3
删除后:
[255.255.11.135, 255.255.111.35]

进程已结束,退出代码0

4.每一次递归调用backtrack方法代表什么?

每一次递归调用 backtrack 方法代表了一次在生成IP地址过程中的一种可能性的搜索。

在这个算法中,backtrack 方法是一个递归函数,用于生成所有可能的IP地址形式。每次调用 backtrack 方法,都会处理当前段,并尝试将当前段添加到IP地址字符串中。然后,根据当前段的情况,进行下一段的递归调用或回溯。

具体来说,每次调用 backtrack 方法时,会检查以下情况:

  1. 如果已经找到了4个段并且遍历完所有数字,即生成了一个合法的IP地址,将它添加到结果列表中。

  2. 如果当前索引超出了字符串的长度,表示已经遍历完了所有数字,直接返回,避免继续拼接多余的数字段。

  3. 否则,根据当前索引开始尝试截取1到3个数字作为当前段,并进行合法性判断。

    • 如果截取的段以0开头且长度大于1,跳过该段,因为以0开头的多位数字是不合法的IP地址段。

    • 如果截取的段转换为数字后在合法的范围内(0到255),将当前段添加到IP地址字符串中,并进行下一段的递归调用。

    • 如果截取的段不在合法的范围内,停止继续尝试更长的段,并回溯到上一层。

通过递归调用 backtrack 方法,我们可以遍历所有可能的组合,生成符合条件的IP地址形式。每一次递归调用都代表了一种搜索路径,探索了一种可能的IP地址形式。当递归返回时,会继续尝试其他可能的组合,直到遍历完所有可能性。

 5.ipAddress.delete(ipAddress.length() - i - (segmentCount > 0 ? 1 : 0), ipAddress.length()); 这段代码是什么意思

这段代码用于回溯时将添加的当前段从IP地址字符串 ipAddress 中删除,以便继续搜索其他可能的组合。

让我们逐步解析这段代码的含义:

  • ipAddress.length(): 返回当前IP地址字符串的长度。

  • i: 当前段的长度,表示要删除的字符数。

  • segmentCount > 0 ? 1 : 0: 三元表达式用于判断当前段是否为第一个段,如果是第一个段,需要额外删除一个点号的字符。

  • ipAddress.length() - i - (segmentCount > 0 ? 1 : 0): 计算要删除的字符起始位置的索引。

  • ipAddress.length(): 计算要删除的字符结束位置的索引。

因此,这段代码的作用是删除之前在回溯过程中添加到IP地址字符串中的当前段,以便回溯到上一层继续搜索其他可能的组合。它根据当前段的长度和是否为第一个段,计算出要删除的字符的起始位置和结束位置,然后使用 delete 方法删除这段字符。

例如,如果当前段是长度为3的数字字符 "123",并且不是第一个段,则 ipAddress.length() - i 将得到要删除的字符的起始位置的索引,而 ipAddress.length() 将得到要删除的字符的结束位置的索引。通过调用 delete 方法,从IP地址字符串中删除这段字符,以便在回溯时重新拼接其他可能的组合。

这个操作是回溯算法中的一部分,用于保持IP地址字符串的正确状态,以便正确生成所有可能的IP地址形式。

 6.哪一部分体现了“剪枝”

在这段代码中,剪枝操作体现在以下两个地方:

  1. 在截取每个段的数字之前,进行了一系列的判断和排除:

    • 检查当前索引和截取长度是否超出字符串的长度,如果是则直接跳出循环,避免无效的组合。

    • 检查截取的段是否以0开头且长度大于1,如果是则直接跳过该段,因为以0开头的多位数字是不合法的IP地址段。

    • 检查截取的段转换为数字后是否在合法的范围内(0到255),如果不在范围内则直接跳过,因为这样的数字也无法构成合法的IP地址段。

  2. 在回溯过程中,当找到了一个合法的IP地址形式后,会立即将它添加到结果列表中,并回溯到上一层继续搜索其他可能的组合。这样可以避免不必要的递归调用和搜索。

这些剪枝操作的目的是排除不符合条件的组合,减少搜索空间,提高算法效率。通过剪枝,可以避免生成大量无效的IP地址形式,只保留符合要求的组合,从而加快算法的执行速度。

7.这段代码的思想是什么?

这段代码的思想是使用回溯算法(backtracking)来生成所有可能的IP地址形式。

主要思路如下:

  1. 创建一个空列表 result 用于存储所有可能的IP地址。

  2. 首先检查输入的字符串 digits 是否符合生成IP地址的条件,即字符串长度在4到12之间。

  3. 调用 backtrack 方法进行回溯搜索,传入初始参数:结果列表 result、输入字符串 digits、IP地址的每个段的字符串 ipAddress、当前段数 segmentCount、当前索引 index

  4. backtrack 方法中,首先检查是否找到了一个合法的IP地址:如果已经有4个段并且遍历完所有数字,就将当前的 ipAddress 添加到结果列表 result 中。

  5. 然后,在每个段中尝试截取1到3个数字,并进行以下判断:

    • 如果当前索引超出了字符串长度,结束循环。

    • 如果截取的段以0开头且长度大于1,排除该情况,因为以0开头的多位数字是不合法的IP地址段。

    • 将截取的段转换为数字,并检查数字是否在合法的范围内(0到255)。

  6. 如果通过了上述检查,将当前段添加到IP地址的字符串表示 ipAddress 中。

  7. 递归调用 backtrack 方法,处理下一个段,更新参数:段数加1,索引更新为 index + i

  8. 递归结束后,进行回溯,将添加的当前段从IP地址字符串 ipAddress 中删除,以便继续搜索其他可能的组合。

  9. 最终,返回结果列表 result

8.IP地址,有什么格式要求?

IP地址是一种用于标识和定位网络设备的地址。它遵循特定的格式要求:

  1. IP地址由四个用点分隔的十进制数字组成,每个数字范围从0到255之间(包括0和255)。

  2. 每个数字不能有前导零,除非该数字本身为0。

  3. IP地址的有效长度为32位。

  4. 示例有效的IP地址格式:192.168.0.1、10.0.0.255、172.16.254.1等。

  5. 示例无效的IP地址格式:256.0.0.1(超出了每个数字的范围)、01.23.45.67(有前导零)等。

总结起来,IP地址的格式要求是四个十进制数字以点分隔,每个数字在0到255之间(无前导零),并且共有32位。

9.图片

​​​​​​​

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值