最长递增子序列的一点延伸:信封嵌套

136 阅读2分钟

一、题目

image.png

思路分析:

  1. 如果说在w维度上,wi是互不相同的,那么只需要对排序后的二维数组的h维度求一个最长递增子序列的长度即可
  2. 需要特别考虑wi相同的这部分宽度相同的一组信封,不可能相互嵌套,也就是只能从中选取一个信封
  3. 如何保证这一点?当宽度相同时,高度降序排序。比如说一组信封[1,2],[1,3],[1,4],如果按照升序排序,会出现[1,2]->[1,3]->[1,4]相互嵌套的问题,但是按照降序排序,4,3,2就不可能相互组成一个递增的序列。

代码实现

class Solution {
    public int maxEnvelopes(int[][] envelopes) {
        // 排序
        Arrays.sort(envelopes, new Comparator<int[]>(){
            @Override
            public int compare(int[] o1, int[] o2){
                return o1[0]==o2[0]?o2[1]-o1[1]:o1[0]-o2[0];
            }
        });
        // 选取hi作为求最长递增子序列的数组
        int[] heigth = new int[envelopes.length];
        for(int i=0;i<envelopes.length;i++){
            heigth[i] = envelopes[i][1];
        }
        return LISLength(heigth);
    }

    public int LISLength(int[] heigth){
        int[] dp = new int[heigth.length];
        dp[0]=heigth[0];
        int len = 0;
        for(int i=0;i<heigth.length;i++){
            // 大于最后一个堆顶元素 直接插入
            if(heigth[i]>dp[len]){
                len++;
                dp[len]=heigth[i];
            }
            else{
                int left = 0;
                int right = len;
                // 二分法
                while(left<right){
                    int mid = left+(right-left)/2;
                    if(dp[mid]<heigth[i]){
                        left = mid+1;
                    }else{
                        // 最左边界
                        right = mid;
                    }
                }
                // 可以保证height[i]<=dp[len] 一定会有一个插入位置
                dp[left] = heigth[i];
            }
            
        }
        // 返回dp数组的长度:当前索引下标+1
        len++;
        return len;
    }
}
  • 时间复杂度:O(nlogn),数组排序的时间复杂度为O(nlogn),遍历数组并对其进行二分查找的时间复杂度也是O(nlogn)
  • 空间复杂度:O(n) dp数组和height数组

二、匿名内部类

  • 内部类:定义在类中的类,主要作用是将逻辑相关的类放在其中
  • 匿名内部类:一种特殊的内部类,没有类名,在继承一个父类或实现一个接口的同时生成该类的一个对象(由于不用在其他地方用到,不需要类名)

匿名内部类的一些使用场景

  • 为Thread传入一个Runnable对象
Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        
    }
});

查看Runnable接口的源码:

image.png 可以看到Runnable被标记为函数式接口,所谓函数式接口,指的是只有一个抽象方法的接口。而lambda表达式的使用场景是:“任何有函数式接口的地方”

  • 在Arrays.sort()方法中传入一个Comparator对象
Arrays.sort(envelopes, new Comparator<int[]>() {
    @Override
    public int compare(int[] o1, int[] o2) {

    }
});

三、lambda表达式

假设有一个类Item,现在需要根据Item的价格、评分筛选出符合条件的对象:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Item {
    // 商品名称
    private String title;
    // 商品价格
    private Double price;
    // 商品评分
    private Integer level;
}

1、写两个根据条件过滤Item的方法

public class Test {
    public static void main(String[] args) {
        List<Item> itemList = new ArrayList<>();
        itemList.add(new Item("茶叶",118.50,5));
        itemList.add(new Item("纸巾",39.99,4));
        itemList.add(new Item("鼠标",59.00,3));
        // 筛选条件:价格低于100
        filterItemByPrice(itemList,100.00);
        System.out.println("---------------");
        filterItemByLevel(itemList,4);
    }

    /**
     *
     * @param itemList 对商品列表进行筛选
     * @param condition 筛选条件
     * @return 符合条件的商品列表
     */
    public static void filterItemByPrice(List<Item> itemList, double condition){
        List<Item> item = new ArrayList<>();
        for (Item item1:itemList){
            if (item1.getPrice()<condition){
                item.add(item1);
            }
        }
        printItem(item);
    }

    public static void filterItemByLevel(List<Item> itemList, Integer condition){
        List<Item> item = new ArrayList<>();
        for (Item item1:itemList){
            if (item1.getLevel()>condition){
                item.add(item1);
            }
        }
        printItem(item);
    }

    public static void printItem(List<Item> items){
        items.forEach(System.out::println);
    }
}

image.png

可以发现,这两个方法的逻辑非常相似,可以纵向抽取出来定义一个接口,让子类各自实现具体的逻辑:

2、定义接口

// 接口中定义一个抽象方法实现筛选
public interface ItemFilter<T> {

    public boolean filter(Item item,T condition);
}

// 实现根据价格筛选
public class ItemFilterByPrice implements ItemFilter{

    @Override
    public boolean filter(Item item, Object condition) {
        return item.getPrice()<(double)condition;
    }
}

// 实现根据评分筛选
public class ItemFilterByLevel implements ItemFilter{
    @Override
    public boolean filter(Item item, Object condition) {
        return item.getLevel()>(Integer) condition;
    }
}


/**
 * 封装一个传入过滤器和条件返回符合条件的商品列表函数
 * @param itemList 传入商品列表
 * @param itemFilter 过滤器
 * @param condition 筛选条件
 * @return
 */
public static List<Item> getByFilter(List<Item> itemList,ItemFilter itemFilter,Object condition){
    List<Item> ans = new ArrayList<>();
    for (Item item:itemList){
        if (itemFilter.filter(item,condition)){
            ans.add(item);
        }
    }
    return ans;
}

image.png

但是,接口ItemFilter中的filter方法的逻辑非常简单,我们是不是可以在调用的时候直接实现这个接口?并且只需要使用一次,也就是不需要持有这个对象的引用,不需要类名,这个场景就适合使用匿名内部类了。

3、匿名内部类

public static void main(String[] args) {
    List<Item> itemList = new ArrayList<>();
    itemList.add(new Item("茶叶",118.50,5));
    itemList.add(new Item("纸巾",39.99,4));
    itemList.add(new Item("鼠标",59.00,3));
    // 筛选条件:价格低于100
    // 使用时直接实现该抽象类
    List<Item> itemList1 = getByFilter(itemList, new ItemFilter() {
        @Override
        public boolean filter(Item item, Object condition) {
            return item.getPrice()<(Double) condition;
        }
    },100.00);

    List<Item> itemList2 = getByFilter(itemList, new ItemFilter() {
        @Override
        public boolean filter(Item item, Object condition) {
            return item.getLevel()<(Integer) condition;
        }
    },4);
    printItem(itemList1);
    System.out.println("-----------");
    printItem(itemList2);
}

image.png

接口ItemFilter中只有一个抽象方法,是不是很符合函数式接口的定义?而lambda表达式的使用场景就是“一切有函数式接口的地方”。

4、函数式接口和lambda表达式

public static void main(String[] args) {
    List<Item> itemList = new ArrayList<>();
    itemList.add(new Item("茶叶",118.50,5));
    itemList.add(new Item("纸巾",39.99,4));
    itemList.add(new Item("鼠标",59.00,3));
    // 筛选条件:价格低于100
    // 实现函数式接口可以使用lambda表达式
    List<Item> itemList1 = getByFilter(itemList,((item, condition) -> {
        return item.getPrice()<(Double) condition;
    }),100.00);
    List<Item> itemList2 = getByFilter(itemList,((item, condition) -> {
        return item.getLevel()>(Integer) condition;
    }),4);
    printItem(itemList1);
    System.out.println("-----------");
    printItem(itemList2);
}

image.png

四、总结

匿名内部类、函数式接口、lambda表达式是Java中很重要的语言特性,以前缺少编码经验,对这些的理解都停留在概念上。从一个比较器中突然明白,这不就是一个匿名内部类吗?匿名内部类不需要类名,在调用时直接实现一个接口或继承一个父类。而函数式接口是只有一个抽象方法的接口,因为只有一个,所以使用lambda表达式那样简略的写法也可以确定调用的是哪一个方法