阅读视图

发现新文章,点击刷新页面。
☑️ ⭐

两数组元素之差最小绝对值

题目

  • 给定两个整数数组,从两个数组中各取一个元素,要求它们的差值绝对值最小

代码

1. 先排序,后双指针

import java.util.Arrays;public class Main {    public static void main(String[] args) {        int[] arr1 = {1, 3, 5, 11};        int[] arr2 = {4, 7, 9};        Arrays.sort(arr1);        Arrays.sort(arr2);        int i = 0, j = 0;        int minDiff = Integer.MAX_VALUE;        int num1 = -1, num2 = -1;        while (i < arr1.length && j < arr2.length) {            int diff = Math.abs(arr1[i] - arr2[j]);            if (diff < minDiff) {                minDiff = diff;                num1 = arr1[i];                num2 = arr2[j];            }            // 如果不需要记录两个数分别是多少,可以直接使用以下语句            // minDiff = Math.min(minDiff, Math.abs(arr1[i] - arr2[j]));            if (arr1[i] < arr2[j]) {                i++;            } else {                j++;            }        }        System.out.println("两个数组中差值绝对值最小的元素是 " + num1 + " 和 " + num2);        System.out.println("它们的差值绝对值是 " + minDiff);    }}
🔲 ⭐

多读单写场景下的thread-safe的map

题目

基于HashMap,实现多读单写场景下的thread-safe的map

代码

import java.util.HashMap;import java.util.Map;import java.util.concurrent.Semaphore;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;/** * 思路1:借助 ReadWriteLock 实现多读单写的线程安全map */class MultiReadSingleWriteMap1<K, V> {    private final Map<K, V> map = new HashMap<>();    // 使用ReadWriteLock实现读写锁    // 读操作使用读锁 readLock(),写操作使用写锁 writeLock()    private final ReadWriteLock lock = new ReentrantReadWriteLock();    /**     * get操作     * 类型:读     */    public V get(K key) {        // 读取时允许多个线程同时读取        lock.readLock().lock();        try {            return map.get(key);        } finally {            lock.readLock().unlock();        }    }    /**     * put操作     * 类型:写     */    public void put(K key, V value) {        // 写入时只有一个线程可以访问 Map        lock.writeLock().lock();        try {            // 新增/修改目标值            map.put(key, value);        } finally {            // 释放写锁            lock.writeLock().unlock();        }    }    /**     * remove操作     * 类型:写     */    public void remove(K key) {        // remove属于写操作,只有一个线程可以访问Map        lock.writeLock().lock();        try {            // 删除目标值            map.remove(key);        } finally {            // 释放写锁            lock.writeLock().unlock();        }    }}/** * 思路2:借助 synchronized、信号量 实现多读单写的线程安全map */class MultiReadSingleWriteMap2<K, V> {    // 定义读信号量、写信号量    private final Semaphore readLock;    private final Semaphore writeLock;    // 定义读计数器,用于判断占据和释放写锁的时机    private int readCount;    // 定义用于实际存储键值对的Map    private final Map<K, V> map = new HashMap<>();    /**     * 构造函数     */    public MultiReadSingleWriteMap2(int readLimit) {        this.readCount = 0;        // 读信号量初始化为输入上限值        this.readLock = new Semaphore(readLimit);        // 写信号量初始化上限为1        this.writeLock = new Semaphore(1);    }    /**     * get操作     * 类型:读     */    public V get(K key) throws InterruptedException {        // 获取读信号量        readLock.acquire();        try {            // 使用synchronized锁住临界区,对读计数器操作            synchronized (this) {                // 拿到读信号量,读线程计数器加1,占据写信号量                readCount ++;                if (readCount == 1) {                    // 因为写信号量只有1个,所以在读计数器等于1时占据一次就足够                    writeLock.acquire();                }            }        } finally {            readLock.release();        }        // 读取目标值        V value = map.get(key);        // 重新获取读信号量        readLock.acquire();        try {            synchronized (this) {                // 读取完毕,读线程计数器减1                readCount --;                // 如果读线程计数器为0,没有线程在读,可以释放写信号量                if (readCount == 0) {                    writeLock.release();                }            }        } finally {            readLock.release();        }        return value;    }    /**     * put操作     * 类型:写     */    public void put(K key, V value) throws InterruptedException {        // 尝试获取写信号量        writeLock.acquire();        try {            // 新增/修改目标值            map.put(key, value);        } finally {            // 释放写信号量            writeLock.release();        }    }    /**     * remove操作     * 类型:写     */    public void remove(K key) throws InterruptedException {        // 尝试获取写信号量        writeLock.acquire();        try {            // 删除目标值            map.remove(key);        } finally {            // 释放写信号量            writeLock.release();        }    }}
🔲 ☆

全排列问题(非递归实现和递归实现)

题目

  • 给定一个不含重复数字的数组 nums ,返回其所有可能的全排列

代码

1. 非递归实现,借助栈

import java.util.ArrayList;import java.util.List;import java.util.Stack;/** * @author: Li Qiang * @date: 2023/7/26 * @description: 全排列问题的非递归实现 */public class Permutations {    public static void main(String[] args) {        Solution solution = new Solution();        int[] nums = {1, 2, 3, 4};        List<List<Integer>> permutations = solution.permute(nums);        // 输出验证        for (List<Integer> permutation : permutations) {            System.out.println(permutation);        }    }}class Solution {    public List<List<Integer>> permute(int[] nums) {        int n = nums.length;        // 定义结果列表        List<List<Integer>> ans = new ArrayList<>();        // 借助栈模拟递归过程        Stack<List<Integer>> stack = new Stack<>();        stack.push(new ArrayList<>());        while (!stack.isEmpty()) {            // currentPath代表当前的排列(不一定是全排列)            // 其中的元素代表已将被当前排列采用的元素            List<Integer> currentPath = stack.pop();            if (currentPath.size() == n) {                // 如果当前的排列是全排列,将其添加到结果列表中                ans.add(currentPath);            } else {                // 如果当前的排列没有完成,通过添加未使用的元素来补充它                for (int element : nums) {                    if (!currentPath.contains(element)) {                        // 如果当前排列的元素中不包含element,则采纳当前数组元素并将其放进新排列中                        List<Integer> newPath = new ArrayList<>(currentPath);                        newPath.add(element);                        // 新的排列(不一定是全排列)暂时压入栈中                        stack.push(newPath);                    }                }            }        }        return ans;    }}

2. 递归实现

/** * @author: Li Qiang * @date: 2023/7/26 * @description: 全排列问题的递归实现 */class Solution {    List<List<Integer>> ans;    boolean[] visited;    List<Integer> path;    public List<List<Integer>> permute(int[] nums) {        int n = nums.length;        ans = new ArrayList<>();        visited = new boolean[n];        path = new ArrayList<>();                loop(nums, 0, n);        return ans;    }    public void loop(int[] nums, int len, int n) {        if (len == n) {            ans.add(new ArrayList<>(path));            return;        }        for (int i = 0; i < n; i ++) {            if (visited[i] == true) {                continue;            } else {                visited[i] = true;                path.add(nums[i]);                loop(nums, len + 1, n);                // 回溯                path.remove(path.size() - 1);                visited[i] = false;            }        }    }}
🔲 ☆

SpringBoot项目使用OSHI获取系统状态信息报错

SpringBoot项目,使用OSHI获取系统状态信息时,后端打印正常,返回至前端的过程中遇到了以下两个报错

  1. java.lang.IllegalStateException: Unmapped relationship: 7
  2. COM exception querying MSAcpi_ThermalZoneTemperature, which might not be on

解决方式如下:

1. java.lang.IllegalStateException: Unmapped relationship: 7

解决方式:添加依赖net.java.dev.jna

<!-- OSHI --><dependency>    <groupId>com.github.oshi</groupId>    <artifactId>oshi-core</artifactId>    <version>5.2.5</version></dependency><!-- jna --><dependency>    <groupId>net.java.dev.jna</groupId>    <artifactId>jna-platform</artifactId>    <version>5.10.0</version></dependency>

2. COM exception querying MSAcpi_ThermalZoneTemperature, which might not be on

解决方式:将获取到的系统状态信息转为String或者整型后再添加到VO中返回

(1)业务逻辑代码
import com.geo.integrated.model.vo.SystemStatus;import com.geo.integrated.service.VisualStatusService;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;import oshi.SystemInfo;import oshi.hardware.*;import oshi.software.os.OperatingSystem;@Service@Slf4jpublic class VisualStatusServiceImpl implements VisualStatusService {    /**     * 获取系统状态信息     *     * @return 系统状态信息     */    @Override    public SystemStatus getSystemState() {        // 系统信息        SystemInfo systemInfo = new SystemInfo();        // 操作系统信息        OperatingSystem operationSystemInfo = systemInfo.getOperatingSystem();        // 硬件信息        HardwareAbstractionLayer hardwareInfo = systemInfo.getHardware();        /*有了代表硬件信息的对象HardwareAbstractionLayer之后,就可以获取硬件相关的信息了*/        // 内存相关信息        GlobalMemory memoryInfo = hardwareInfo.getMemory();        // 内存总容量        String totalMemory = String.valueOf(memoryInfo.getTotal());        // 可用内存的容量        String availableMemory = String.valueOf(memoryInfo.getAvailable());        /*有了内存总容量和内存可用容量,就可以计算出当前内存的利用率了*/        // CPU相关信息        CentralProcessor processor = hardwareInfo.getProcessor();        // CPU型号        String processorName = processor.getProcessorIdentifier().getName();        // 物理CPU数        int physicalPackageCount = processor.getPhysicalPackageCount();        //物理核心数        int physicalProcessorCount = processor.getPhysicalProcessorCount();        SystemStatus systemStatus = new SystemStatus(String.valueOf(systemInfo),                String.valueOf(operationSystemInfo),                String.valueOf(hardwareInfo),                String.valueOf(memoryInfo),                Long.parseLong(totalMemory) / 1000 / 1000 / 1000,                Long.parseLong(availableMemory) / 1000 / 1000 / 1000,                String.valueOf(processor), String.valueOf(processorName),                Integer.parseInt(String.valueOf(physicalPackageCount)),                Integer.parseInt(String.valueOf(physicalProcessorCount)));        return systemStatus;    }}
(2)VO代码
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@NoArgsConstructor@AllArgsConstructorpublic class SystemStatus {    /**     * 系统信息     */    private String systemInfo;    /**     * 操作系统信息     */    private String operationSystemInfo;    /**     * 硬件信息     */    private String hardwareInfo;    /**     * 内存相关信息     */    private String memoryInfo;    /**     * 内存总容量     */    private Long totalMemory;    /**     * 可用内存的容量     */    private Long availableMemory;    /**     * CPU相关信息     */    private String processor;    /**     * CPU型号     */    private String processorName;    /**     * 物理CPU数     */    private Integer physicalPackageCount;    /**     * 物理核心数     */    private Integer physicalProcessorCount;}
☑️ ⭐

三个线程按指定顺序打印1到100

题目

  • 创建三个线程,按指定顺序打印1到100,具体如下:

    • 线程1打印1
    • 线程2打印2
    • 线程3打印3
    • 线程1打印4
    • 线程2打印5
    • 线程3打印6
    • ……
    • 线程3打印99
    • 线程1打印100
  • 知识点:wait()/notifyAll()

代码

import java.util.concurrent.atomic.AtomicInteger;/** * @author: LiQiang * @date: 2023/03/30 * @description: 三个线程按顺序打印1到100 */public class Main {    // 锁    private static Object resource = new Object();    // 打印区分标识(值为1~3,初始设置1)    private static int flag = 1;    // 需要打印的值    static AtomicInteger integer = new AtomicInteger(1);    // private static volatile int number = 1;    public static void main(String[] args) {        // 三个线程按顺序打印1~100        new Thread(new Runnable() {            @Override            public void run() {                while (integer.get() <= 100) {                    synchronized (resource) {                        while (flag != 1) {                            try {                                resource.wait();                            } catch (InterruptedException e) {                                e.printStackTrace();                            }                        }                        // 打印操作                        if (integer.get() <= 100) {                            System.out.println(Thread.currentThread().getName() + " --- " + integer.getAndIncrement());                        } else {                            Thread.currentThread().interrupt();                            // break;                        }                        // number ++;                        // 变更标识                        flag = 2;                        // 通知线程2                        resource.notifyAll();                    }                }            }        }, "线程1").start();        new Thread(new Runnable() {            @Override            public void run() {                while (integer.get() <= 100) {                    synchronized (resource) {                        while (flag != 2) {                            try {                                resource.wait();                            } catch (InterruptedException e) {                                e.printStackTrace();                            }                        }                        // 打印操作                        if (integer.get() <= 100) {                            System.out.println(Thread.currentThread().getName() + " --- " + integer.getAndIncrement());                        } else {                            Thread.currentThread().interrupt();                            // break;                        }                        // 变更标识                        flag = 3;                        // 通知线程3                        resource.notifyAll();                    }                }            }        }, "线程2").start();        new Thread(new Runnable() {            @Override            public void run() {                while (integer.get() <= 100) {                    synchronized (resource) {                        while (flag != 3) {                            try {                                resource.wait();                            } catch (InterruptedException e) {                                e.printStackTrace();                            }                        }                        // 打印操作                        if (integer.get() <= 100) {                            System.out.println(Thread.currentThread().getName() + " --- " + integer.getAndIncrement());                        } else {                            Thread.currentThread().interrupt();                            // break;                        }                        // 变更标识                        flag = 1;                        // 通知线程1                        resource.notifyAll();                    }                }            }        }, "线程3").start();    }}
☑️ ⭐

使用Python获取国自然项目数据

需求

从指定网站中获取不同分类下的所有国自然项目数据,如优青、杰青、面上、重大等分类中的项目数据。

依赖包

  • urllib3
  • time
  • csv

代码

整个过程分为数据获取和数据处理两部分,具体代码如下。

1.网页原始数据获取

import urllib3import timefrom urllib.parse import unquotedef download_content(url):    '''    根据链接下载数据    :param url: 链接    :return: 页面数据    '''    http = urllib3.PoolManager()    # 不登录只能查看20页,所以总页数需要通过查询条件限制在20页以内,建议通过起止时间来限制,便于分类和获取    body = {'startTime': '2016', 'endTime': "2021",            'addcomment_s1': 'D',            'searchsubmit': 'true',            'subcategory': '优秀青年基金项目'}    # letpub网站是POST请求,izaiwen网站(已停用)是GET请求    response = http.request('POST', url, fields=body)    # 从返回结果中获取页面内容,编码一般是utf-8    response_data = response.data    html_content = response_data.decode('utf-8')    unquote(html_content)    # print(html_content)    return html_contentdef save_to_file(filename, content):    '''    保存数据到本地文件    :param filename: 文件名    :param content: 文件内容    :return: 本地文件    '''    fo = open(filename, 'w', encoding='utf-8')    fo.write(content)    fo.close()if __name__ == '__main__':    # url需要根据在letpub网站中点击查询时,控制台中显示的实际地址`Request URL`修改    url_prefix = 'https://www.letpub.com.cn/nsfcfund_search.php?mode=advanced&datakind=list&currentpage='    # 总页数根据网页查询结果调整    page_total = 18    # 保存路径名建议根据查询条件按年份区分    path_prefix = './2016-2021/page-'    path_suffix = '.html'    # 从第一页开始获取,中间加休眠5~10秒左右,防止访问被禁    page_number = 1    while page_number <= page_total:        # 每页的url        url = url_prefix + str(page_number)        # 每页的原始数据        page_data = download_content(url)        # 每页的保存路径        save_path = path_prefix + str(page_number) + path_suffix        # 保存每页的数据        save_to_file(save_path, page_data)        print('page ' + str(page_number) + ' end')        # 休眠10秒        time.sleep(10)        # 访问下一个页面        page_number += 1

2.数据处理

import csvdef save_to_file(filename, content):    fo = open(filename, "w", encoding="utf-8")    fo.write(content)    fo.close()def replace_string(content):    '''    页面数据处理方法(根据网站页面内容自定义,以letpub网站举例)    :param content: 页面内容    :return: 处理后的页面内容    '''    '''    页面数据分割的形式,可以打开原始的页面数据文件看看,找易于分割出需要内容的字符串,像letpub网站有下面两行明显标识,很好分割    '''    content = content.split('<!-- ################## start content ################## -->')[1]    content = content.split('<!-- ################## end content ################## -->')[0]    content = content.split('TR>')[2]    '''    替换页面中重复多次出现的标签    '''    # 此处标签替换为换行符\n是为了区分不同的项目(通常是每页10个项目)    content = content.replace('<tr style="background:#EFEFEF;">', '\n')    content = content.replace(        '<td style="border:1px #DDD solid; border-collapse:collapse; text-align:left; padding:8px 8px 8px 8px;">', '')    content = content.replace(        'style="border:1px #DDD solid; border-collapse:collapse; text-align:left; padding:8px 8px 8px 8px;  font-size:13px; color:#333333;">',        '')    content = content.replace(        '<td style="border:1px #DDD solid; border-collapse:collapse; text-align:left; padding:8px 8px 8px 8px; font-size:13px; color:#333333;"',        '')    # 项目小标题对于实际内容获取没有帮助,直接替换掉;若需要保留,可以只替换</td>标签    content = content.replace('题目</td>', '')    content = content.replace('学科分类</td>', '')    content = content.replace('学科代码</td>', '')    content = content.replace('执行时间</td>', '')    content = content.replace('中文关键词</td>', '')    content = content.replace('英文关键词</td>', '')    content = content.replace('结题摘要</td>', '')    # 此处的替换字符串@@可以修改为任意其他的可以唯一区分的字符串(如##、#%),不建议使用空格、反斜杠等容易出现内容歧义的字符    content = content.replace('</td>', '@@')    content = content.replace('<td', '')    content = content.replace(        'style="border:1px #DDD solid; border-collapse:collapse; text-align:left; padding:8px 8px 8px 8px; color:#3b5998; font-weight:bold;">',        '')    content = content.replace('colspan="6">', '')    content = content.replace('</tr>', '')    content = content.replace('<tr>', '')    '''    单独处理页面中剩余的非通用标签,如下方的表头    '''    content = content.split('批准年份')[1]    content = content.replace('</th>', '')    content = content.replace('<', '')    content = content.replace('</th>', '')    content = content.replace('  ', '')    content = content.replace(' ', '')    # 返回结果    return contentif __name__ == '__main__':    '''    以下参数需要手动指定,以按年份查询区分为例    '''    # 页面文件的存储路径    origin_path_name = './2016-2021/'    # 文件前缀、后准、页面数量    file_name_prefix = 'page-'    file_name_suffix = '.html'    file_number = 18    # 结果输出的目标路径    target_path_name = './flushed_data/2016-2021/'    # 处理后文件的前缀、后缀    target_file_name_prefix = 'flushed-page-'    target_file_name_suffix = '.txt'    '''    需要从页面数据中获取的常用的信息    '''    # 负责人    charge_list = []    # 单位    department_list = []    # 金额 (万)    money_list = []    # 项目编号    project_number_list = []    # 项目类型    project_type_list = []    # 所属学部    xuebu_list = []    # 批准年份    pass_year_list = []    # 题目    title_list = []    # 学科分类    subject_type_list = []    # 学科代码    subject_code_list = []    # 执行时间    execution_time_list = []    # 记录所有页面中的每一行(因为预处理时不可以替换的换行符\n的存在,会出现空行,通过长度判断筛选掉)    lines = []    # 从第1个页面开始处理    page_number = 1    while page_number <= file_number:        # 处理后的页面内容        content = ''        # 页面文件名        file_name = origin_path_name + file_name_prefix + str(page_number) + file_name_suffix        # 打开文件获取数据,按行追加到页面内容中        with open(file_name, 'r+', encoding='utf-8') as f:            for line in f:                content += line        f.close()        # 页面数据处理(方法内容自定义,需要根据页面实际内容调整)        content = replace_string(content)        # print(content)        # 保存各处理后的页面内容到文件中        save_path = target_path_name + target_file_name_prefix + str(page_number) + target_file_name_suffix        save_to_file(save_path, content)        # 打开上一步保存的处理后的页面数据文件        with open(save_path, 'r', encoding='utf-8') as f:            # 将符合筛选要求的行内容添加到列表中(因为预处理时不可以替换的换行符\n的存在,会出现空行,通过长度判断筛选掉)            for line in f:                if (len(line) > 5):                    line = line.replace('\n', '')                    lines.append(line)        f.close()        # 下一个页面        page_number += 1    # 处理列表信息,分到各个单独的列表中    for line in lines:        # 可能因为‘结题摘要’等小项的存在,导致出现一些冗余行,先打印结果看看,在循环中加个筛选        if line.count('@@') >= 9:            # 页面数据处理方法中人为添加的分隔符做分割,按照网页数据中固定顺序分到不同的列表中            items = line.split('@@')            # print(items)            charge_list.append(items[0])            department_list.append(items[1])            money_list.append(items[2])            project_number_list.append(items[3])            project_type_list.append(items[4])            xuebu_list.append(items[5])            pass_year_list.append(items[6])            title_list.append(items[7])            subject_type_list.append(items[8])            subject_code_list.append(items[9])            execution_time_list.append(items[10])    # 把上方的列表合并为一个table    content_to_table = zip(charge_list, department_list, money_list, project_number_list, project_type_list, xuebu_list,                           pass_year_list, title_list, subject_type_list, subject_code_list, execution_time_list)    # 把数据保存到表格中(csv文件如果用word打开乱码,可以用wps打开)    output_table_path = origin_path_name.split('/')[1] + '.csv'    with open(output_table_path, 'w', newline='', encoding='utf-8') as f:        writer = csv.writer(f)        for row in content_to_table:            writer.writerow(row)    f.close()
☑️ ⭐

pip uninstall scikit-learn报错,Cannot uninstall 'scikit-learn'

执行命令 pip uninstall scikit-learn 报错,Cannot uninstall ‘scikit-learn’

具体报错如下:

ERROR: Cannot uninstall 'scikit-learn'. It is a distutils installed project and thus we cannot accurately determine which files belong to it which would lead to only a partial uninstall.
不能卸载“scikit-learn”。这是一个安装了distutils的项目,因此我们不能准确地确定哪些文件属于它,这将导致只有部分卸载。

原因

大概率是其他依赖中包含了scikit-learn,如kerastensorflow

解决办法:

先从本地文件夹中搜索如kerastensorflow等包中是否包含scikit-learn。
若是,则先把这些包卸载掉,然后,找到并删除虚拟环境(以anaconda的虚拟环境为例)中的scikit-learn * .egg-info文件。
最后,重新安装需要版本的scikit-learn,再安装其他被卸载的依赖包(如keras)即可。

🔲 ☆

Estimating High-Dimensional Directed Acyclic Graphs with the PC-Algorithm

原文:Kalisch M, Buehlmann P. Estimating high-dimensional directed acyclic graphs with the PC-algorithm. J Mach Learn Res 2007;8:613–36.
原文网页版
Web of science

Abstract

本文研究了PC算法用于估计具有相应高斯分布的高维有向无环图(DAG)的骨架和等价类。 对于有许多节点(变量)的稀疏问题,PC算法在计算上是可行的,而且往往非常快,它具有吸引人的特性,可以自动实现高计算效率,作为真实底层DAG的稀疏程度的函数。本文证明了该算法在高维稀疏DAGs中的一致性,并允许节点数量随样本大小n快速增长,对于任何0 < a <∞快如$O(n^a)$。稀疏性假设是相当最小的,只要求DAG中的邻域的阶数低于样本容量n。文章还演示了模拟数据的PC算法。

1. Introduction

图模型是一种用来分析和可视化随机变量之间的条件独立性关系流行的概率工具。模型的主要构建模块是节点(代表随机变量)和边(编码了顶点的条件独立性关系)。随机变量之间的条件独立性结构可以使用马尔可夫性质来探索。

当前的研究兴趣是有向无环图(DAG),它包含有向边而不是无向边,这在一定程度上限制了条件独立性关系。这些图可以应用马尔可夫性质来解释。当忽略DAG的方向,可以得到一个DAG的骨架。通常来说,它和条件独立性图(CIG)是不同的,见2.1节(因此,有向图的估计方法不能简单地借鉴无向的CIG的估计方法)。2.1节中可以看到,骨架可以很容易地解释,从而对数据的依赖结构产生有趣的见解。

由于DAG空间的巨大规模,从数据中估计DAG是困难的,在计算上也是不可行的:可能的DAG的数量在节点数量上是超指数的。然而,针对中小规模节点数量,有一些十分成功的的 搜索-评分方法。例如,搜索空间可能像MWST那样被限制为树结构,或者采用贪婪的搜索方式。如GES (Greedy Equivalent Search, see Chickering, 2002a) 方法所述,贪婪的DAG搜索可以通过利用概率等价关系来优化,且搜索空间可以从单个DAG缩小到等价类。尽管这种方法在中小规模的节点数量情况下似乎很有前途,但它受限于一个事实,即等价类的空间在节点数量增长时也是超指数增长的 (Gillispie and Perlman, 2001)。

一个有趣的替代贪婪或者结构限制的方法是Spirtes等人在2000年提出的PC算法。它从一个完备的无向图开始,基于条件独立性决策递归地删除边。这会生成一个无向图,然后它会被部分地定向,并进一步扩展以表示底层的DAG。PC算法在最坏的情况下是以运行时间是运行的,但是如果真实的底层DAG是稀疏的(这通常是一个合理的假设),运行时间将会缩减为多项式时间。

在过去,提出了一些有趣的混合方法,最近,Tsamardinos等人(2006)提出了一种计算上非常有竞争力的算法。本文还参考了他们的论文,在广泛的算法之间进行了相当详尽的数值比较研究。

本文主要研究了在高维环境下DAGs的等价类和骨架的估计(对应于多元高斯分布),即节点数p可能远远大于样本数n。本文证明,当样本大小n→∞时,即使允许维数 $p = p_n = O(n^a) (0 ≤ a <∞)$作为n的函数快速增长,PC算法也能一致地估计出底层稀疏DAG地等价类和骨架。

如第4.5节所示,本文对PC算法的实现速度惊人地快,它允许估计一个稀疏的DAG,即使p很大。对于p远大于n的高维设定,底层DAG的稀疏性对于统计一致性和计算可行性是至关重要的。本文的分析似乎是第一次为高维DAG建立了一个可证明的正确算法(在渐进意义上),该算法在计算上是可行的。

关于包括PC算法在内的一类方法的一致性问题已经被Sprites等人和Robins等人在因果推断的文章讨论过。他们证明,只假设忠实性(第二节中有说明),统一一致性无法实现,但点状一致性可以实现。在本文中,本文用两种方式对其进行了扩展:本文提供了一套假设,使PC算法具有统一的一致性。更重要的是,本文证明即使是当节点数和邻居数增加,并且最小的非零协方差作为样本量的函数而减小,这个一致性也能始终保持。Zhang和Spirtes(2003)也提出了比忠实性条件更严格的假设,使均匀一致成为可能。Zuk等人(2006)对学习正确的贝叶斯网络结构需要多少样本的更普遍的讨论。

寻找DAG的等价类的问题与特征选择问题有很大的重合:如果找到了等价类,则可以很容易地读取任意变量(节点)的马尔可夫毯。给定一个节点集合V,假设M是节点X的马尔科夫毯,那么在给定M的条件下,XV\M是条件独立的。因此,M包含且只包含所有的X的相关特征。例如,见Goldenberg和Moore(2004)关于处理非常高的维度的方法,或Ng(1998)关于处理泛化误差的界限的相当普遍的方法。

2. Finding the Equivalence Class of a DAG

在本节中,首先说明主要的概念。然后,给出关于PC算法的详尽描述。

2.1 Definitions and Preliminaries

G=(V, E) 由一组顶点V={1,…,p} 和一组边 E⊆V×V 组成,即边集是不同节点的有序对的子集。在本文的设定中,节点集对应于随机变量X∈R^p^ 的分量。如果 (i, j)∈E(j, i)/∈E ,则边 (i, j)∈E 被叫做有向边,用符号i→j 表示。如果 (i, j)∈E(j, i)∈E ,则该边被叫做无向边用符号 i-j 表示。一个有向无环图(DAG)是一个所有边都是有向边且不包含环的图。

如果存在有向边 i→j ,则节点i是节点 j 的父节点。节点j的父节点的集合用 pa(j) 表示。节点 j 在图 G 中的邻居集合用 adj(G, j) 表示,它表示所有直接和 j 通过边(有向或者无向)连接的节点。 adj(G, j) 中的节点也被称为j的邻居或者与 j 相邻。

如果 $R^p$上 的概率分布 P 中的条件独立性可以从图 G 的d-separation中被推断出来,反之亦然,则 P 忠诚于图 G 。更精确的说:考虑一个随机向量 X~PP 的忠诚性意味着:对于任意的 i,j∈Vi≠j ,则对于任意的 s⊆V

$X^{(i)}$ and $X^{(j)}$ are conditionally independent given ${X^{(r)}; r ∈ s}$

⇔ node i and node j are d-separated by the set s.

d-separation的概念可以由道德图定义;详见Lauritzen的描述 (1996,Prop. 3.25)。在此指出,忠实性是排除某些类别的概率分布的。Spirtes等人(2000,第3.5.2章)给出了一个非忠实分布的例子。另一方面,多元正态族(本文将限制在此)的非忠诚分布在与DAG G相关的分布空间中形成一个Lebesgue 空集,见Meek(1995a)。

DAG G 的骨架是用无向边代替 G 中的有向边得到的无向图。DAG G 中的 v-structure 是一个有序的三元节点组 (i, j, k) 使得G包含有向边 i→jk→j ,并且 ik 在G中不相邻。

众所周知,对于一个由DAG G 生成的概率分布 P ,存在一个完整的具有对应分布 P 的等价类DAGs (见 Chickering, 2002a, Section 2.2 )。即使有无限多的观察结果,本文也无法区分一个等价类中的不同DAG。利用Verma和Pearl(1990)的一个成果,可以更精确的描述等价类的特征。当且仅当两个DAG有相同的骨架和相同的v-structure时,他们是等价的。

常用的DAG等价类的可视化工具是完备的部分有向无环图(CPDAG)。一个部分有向无环图(PDAG)是一个部分边有向且部分边无向的图。PDAG之间或PDAG和DAG之间的等价性可以通过检查骨架和V形结构来决定,就像DAG一样。一个PDAG是完备的,如果:(1)在属于DAG等价类的每个DAG中也存在相应的有向边,且(2)对于每条无向边 i−j ,在等价类中存在一个带i→j的DAG和一个带 i←j 的DAG。

CPDAG编码了相应的等价类中包含的所有独立性信息。Chickering(2002)证明,当且仅当两个CPDAG表示的是同一个等价类时,它们是等价的,即,它们表示的是同一个等价类。

尽管主要目标是确定CPDAG,骨架本身已经包含了有趣的信息。尤其是,如果概率分布P忠诚于一个DAG G,

there is an edge between nodes i and j in the skeleton of DAG G

⇔ for all $s ⊆ V \ {i, j}, X^{(i)}$ and $X^{(j)}$ are conditionally dependent given ${X^{(r)}; r∈s}$,(1)

(Spirtes et al., 2000, Th. 3.4). 这表明如果概率分布P对于一个DAG G来说是忠诚的,则DAG G的骨架是对应于P的条件独立性图(CIG)的真子集(或子集)。原因是CIG中的一条边只需要在给定集合V \ {i, j} 的情况下有条件依赖性。更重要的是,骨架张的每条边都表示某种强依赖,其不能通过其他变量来解释。本文认为,这个性质对探索性分析很有意义。

在接下来的细节内容中将看到,估计CPDAG主要由两部分组成(这自然地组成了本文地分析结构):(1)骨架的估计(2)部分边的定向。所有统计推断都在第一部分完成,第二部分只是对第一部分的结果应用确定性的规则。因此本文将更多的注意力放在第一部分上,如果第一部分操作正确,那么第二部分将永远不会失败。但是,如果在第一部分中出现错误,第二部分将对它更加敏感,因为它更详细地依赖于(1)的推断结果。当处理高维设定时(p大,n小),CPDAG比骨架的恢复更加困难。此外,对于CPDAG的解释更多地依赖于图的全局正确性。而对于骨架的解释只依赖于局部区域,因此更可靠。

本文的结论是,如果真实的底层概率机制是由DAG生成的,那么找到CPDAG是主要目标。骨架本身通常已经提供了有趣的见解,在高维设定中,当找到一个有用的CPDAG的近似似乎没有希望时,使用无向骨架作为CPDAG的替代目标可能是有趣的。

综上所述,本文接下来将描述两个主要步骤。首先,文章讨论PC算法生成骨架图的部分。之后文章将通过讨论寻找CPDAG的扩展来完成该算法。在讨论PC算法理论性质的时,文章将使用相同的格式。

2.2 The PC-algorithm for Finding the Skeleton

寻找骨架的一种朴素的策略是检查给定所有子集s⊆V \ {i, j}(见公式1)的”条件独立性”,也就是说,如Verma和j . pearl(1991)首次提出的,在多元正态分布情况下的所有”偏相关”。当p大于样本量时,这在计算上是不可行的,在统计上也是不恰当的。PC算法使用了一个更好的办法,它可以利用图的稀疏性。更准确地说,文章应用PC算法的一部分来识别DAG的无向边。

2.2.1 POPULATION VERSION FOR THE SKELETON

在PC算法地population版本中,文章假设对所有必要地条件独立性都有充分地了解。这里指的PC算法是别人所说的PC算法的第一部分;另一部分在第2.3节的算法2中描述。

image-20221028194614633

PC算法的第一部分在Algorithm1中给出。算法1中 l 的最大值可表示为

$m_{reach}$ = maximal reached value of l. (2)

$m_{reach}$的值取决于底层分布。

从Spirtes等人(2000)中的定理5.1中,可以很容易地推导出该算法会生成正确的骨架。文章总结如下。

Proposition 1 考虑一个DAG G并假设分布P忠诚于G。标记最大邻居数为 $q=max_{1≤j≤p}|adj(G, j)|$。然后,$PC_{pop}$ 算法构建DAG的真实骨架。此外,$m_{reach}∈{\lbrace q-1, q\rbrace}$

(原文附录A中给出了证明。)

2.2.2 SAMPLE VERSION FOR THE SKELETON

对于有限的样本,需要估计条件独立性。本文将自身限制在高斯情况下,其中所有节点都对应于具有多元正态分布的随机变量。此外,本文假设忠诚性模型,这意味着条件独立关系对应于d-separation (因此可以从图中读出),反之亦然;见第2.1节。

在高斯情况下,可以从偏相关性中推断出条件的独立性。

Proposition 2 假设随机变量X的分布P是多元正态分布。对于$i ≠ j\in {1,…, p}, k⊆{\lbrace 1,…, p\rbrace}\setminus {\lbrace i, j\rbrace}$,使用符号$\rho_{i,j\mid k}$来表示$X^{(i)}$和$X^{(j)}$在给定 $\lbrace X^{(r)};r\in k \rbrace$时的偏相关性。那么,当且仅当$X^{(i)}$和$X^{(j)}$在给定$\lbrace X^{(r)};r\in k \rbrace $条件独立时$\rho_{i,j\mid k}$的值为0。

证明:该主张是多变量正态分布的一个基本属性,见Lauritzen(1996, Prop. 5.2.)。

因此可以估计偏相关性,以获得条件独立性的估计。样本偏相关性$\hatρ$可以通过回归,部分协方差矩阵的反演,或者使用以下恒等式来递归计算:对于h∈k,

image-20221028195415811

本文使用的是递归方法(上式,即第三种方法)。为了检验偏相关系数是否为0,本文使用Fisher’s z-transform对偏相关系数进行转换。

image-20221028195600738

当使用显著性水平α时,经典决策理论生成了以下规则。$H_0$和$H_A$。当$sqrt(n−|k|−3|)Z(i,j|k)|>Φ^{−1}(1−α/2)$时,接受$H_A:\rho_{i,j|k}≠0$;拒绝$H_0:\rho_{i,j|k}=0$;其中Φ(·)代表N(0, 1)的累积分布函数。

用 $sqrt(n−|k|−3|)Z(i,j|k)| ≤ Φ^{−1}(1−α/2)$替换 $PC_{pop}$算法 中的第11行:“if i and j are conditionally independent given k then”。该算法产生一个依赖于数据的值$\hat{m}_{reach}$,它是公式(2)的样本版本。

$PC_{pop}$ 算法的唯一调优参数是α,这是检验偏相关性的显著性水平。

正如将在第3节中看到的,即使p远大于n,但当DAG是稀疏的,该算法也是渐进一致的。

2.3 Extending the Skeleton to the Equivalence Class

在寻找算法1中的骨架时,记录了使边缘在由S表示的变量中消失的分离集。这对于寻找骨架本身来说是不必要的,但对于将骨架扩展到等价类来说是必不可少的。在算法2中,描述了Pearl(2000, p.50f)的工作,将骨架扩展到属于底层DAG的等价类的CPDAG。

image-20221028200302479

算法2的输出是一个CPDAG,这是由Meek(1995b)首次证明的。

3. Consistency for High-Dimensional Data

由第二节可以看到,首先处理寻找骨架的问题。接下来,将结果扩展到寻找CPDAG。

3.1 Finding the Skeleton

本文将证明第2.2.2节中的PC算法对于DAG的骨架是渐进一致的,即使p远大于n,而DAG是稀疏的。本文假设数据是i.i.d.(独立同分布)的向量$X_1, …, X_n,X_i∈R^p$ 来自有对应分布P的DAG G。为了观察高维行为,本文将使维数作为样本大小的函数增长:因此,$p=p_n$,且$DAG~G=G_n$,分布$P = P_n$。假设如下:

  • (A1)对于任意的n,分布Pn是多元正态分布且忠诚于DAG $G_n$。
  • (A2)维数$p_n=O(n^a),0≤a<∞$
  • (A3)$DAG~G_n$中的最大邻居数表示为 $q_n=max_{1≤j≤p_n}|adj(G,j)|$,与$q_n=O(n^{(1−b)}), 0<b≤1$。
  • (A4)对于集合$k⊆{\lbrace1,…,p_n}\rbrace \setminus {\lbrace i, j\rbrace}$,给定{X^(r)^; r∈k}时,$X^{(i)}$和$X^{(j)}$之间的偏相关用 $ρ_{n;i,j|k}$ 表示。它们的绝对值有上界和下界:image-20221028200721136)(A1)是图模型中常用的假设,尽管它确实限制了可能的概率分布类型(见2.1节的第三段);(A2)允许维度作为样本量的函数的任意多项式增长,即高维度;(A3)是稀疏假设;(A4)是正则性条件。定理1如下图:image-20221028200803279

3.2 Extending the Skeleton to the Equivalence Class

如前所述,所有的推理都是在寻找骨架时完成的。如果这部分完美地完成,也就是说,如果在测试条件独立性时没有错误(假设骨架被正确估计是不够的),第二部分将永远不会失败(见Meek,1995b)。因此,很容易得到定理2:

image-20221028200900068

4. Numerical Examples

本文分析了PC算法使用各种模拟数据集来寻找骨架和CPDAG。利用R语言的pcalg包得到了数值结果。关于对不同算法的广泛的数值比较研究,文章参考了Tsamardinos等人的文章(2006)。

4.1 Simulating Data

4.2 Choice of Significance Level

文章中通过多组对比实验,认为α=0.005或α=0.01时的拟合效果最好。

4.3 Performance for Different Parameter Settings

为了保持概述在一个可管理的大小,文章将后续实验的显著性水平限制为α = 0.01。

4.4 Properties in High-Dimensional Setting

从某种意义上说,DAG的真实边的百分比的稀疏性是下降的,而在另一种意义上,邻域大小的稀疏性是随着n的增加而增加的。

4.5 Computational Complexity

PC 算法的计算复杂度难以精确计算,但最坏情况是以作为维数p的函数的公式(4)为界的。

5. R-Package pcalg

R语言包pcalg可用于从数据中估计DAG的底层骨架或等价类。若要使用此软件包,就需要安装统计软件R。R和R包都可以在http://www.r-project.org免费获得。对于低维问题(但不是对于成百成千上万的p),还有许多pc算法的其他实现也值得一提:

• Hugin at http://www.hugin.com .

• Murphy’s Bayes Network toolbox at http://bnt.sourceforge.net .

• Tetrad IV at http://www.phil.cmu.edu/projects/tetrad .

接下来将展示一个如何使用R包pcalg生成随机DAG,抽取样本并从数据中推断原始DAG的骨架和等价类。结果骨架和CPDAG中的边的线宽可以被调整,以对应于估计得到的依赖关系的可靠性。(线宽与✓(n−|k|−3)Z(i, j, k)的最小值成正比。因此,粗线是可靠的)。

示例代码(原文和新版本R包)

原文版本 Old example in this paper(the new version of R-package)

library(pcalg)## define parameters# number of random variablesp <- 10# number of samplesn <- 10000# sparsness of the graphs <- 0.4 ## generate random dataset.seed(42)# generate a random DAGg <- randomDAG(p, s) # generate random samplesd <- rmvDAG(n, g) ## Note : pcAlgo is DEPRECATED in the new version of pcalg package, and the new method 'pc', 'skeleton' is recommended.# estimate of the skeletongSkel <- pcAlgo(d, alpha=0.05) gCPDAG <- udag2cpdag(gSkel)# The CPDAG can also be estimated directly usinggCPDAG <- pcAlgo(d, alpha=0.05, directed=TRUE)## plot the graphplot(g)plot(gSkel, zvalue.lwd=TRUE)plot(gCPDAG, zvalue.lwd=TRUE)

image-20221028201108702

新的R包版本 New example in the new version of R-package

## Using Gaussian Datalibrary(pcalg)library(Rgraphviz)library(graph)library(BiocGenerics)library(grid)# Load predefined datadata(gmG)n <- nrow(gmG8$x)# labels aka node namesV <- colnames(gmG8$x) # estimate skeletonskel.fit <- skeleton(suffStat = list(C = cor(gmG8$x), n = n), indepTest = gaussCItest, alpha = 0.01, labels = V, verbose = TRUE)if (require(Rgraphviz)) {  ## show estimated Skeleton  par(mfrow=c(1,2))  plot(skel.fit, main = "Estimated Skeleton")  plot(gmG8$g, main = "True DAG")}# estimate CPDAGpc.fit <- pc(suffStat = list(C = cor(gmG8$x), n = n), indepTest = gaussCItest, alpha = 0.01, labels = V, verbose = TRUE)if (require(Rgraphviz)) {  ## show estimated CPDAG  par(mfrow=c(1,2))  plot(pc.fit, main = "Estimated CPDAG")  plot(gmG8$g, main = "True DAG")} 
image-20221028201156455image-20221028201240787

6. Conclusions

本文表明,对于DAG(由CPDAG表示)及其骨架的等价类,PC算法是渐进一致的,具有相应的高维稀疏高斯分布。此外,PC算法对于这种高维、稀疏的问题在计算上是可行的。把这两个事实放在一起,PC算法被确立为一种方法(到目前(文章发表于2007年)为止是唯一的一种),它在计算上是可行的,并且在统一一致性的意义上是可以证明正确的,适用于高维DAGs。稀疏性,即真正底层DAG的邻域的最大大小,对于统计一致性和计算可行性至关重要,其复杂度最多是作为维数函数的多项式。

文章强调,DAG的骨架经常提供有趣的见解,在高维环境下,使用无向的骨架作为更简单但更现实的目标而不是整个CPDAG是非常明智的。

🔲 ☆

LeetCode腾讯精选练习50题-011.盛水最多的容器

题目描述

  • 给定一个长度为 n 的整数数组 height 。
  • 有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
  • 找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
  • 返回容器可以储存的最大水量。
  • 说明:不能倾斜容器。
exampleinput  : [1,8,6,2,5,4,8,3,7]output : 49input  : [1,1]output : 1input  : [4, 4, 2, 11, 0, 11, 5, 11, 13, 8]output : 55

解题思路

  • 双指针+贪心

  • 双指针指向高度数组的首尾两端

  • 容器的面积取决于左右两指针之间的横向距离差左右两指针指向的数字中的较小值

  • 如果向内移动指向的数字较大的那个指针,那么前者左右两指针指向的数字中的较小值不会增加(会不变或者变小),后者左右两指针之间的横向距离差会减小,则两者乘积会减小

  • 因此,移动数字较大的那个指针是不合理的

  • 若想提高容量,能做的就是向内移动高度较小的指针(期望获得更高的高度),并比较移动后的容量是否大于当前最大容量

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

代码(Java)

public class Solution {    public int maxArea(int[] height) {        int left = 0;        int right = height.length - 1;        int maxArea = 0;        while (left < right) {            int area = (right - left) * Math.min(height[left], height[right]);            maxArea = Math.max(area, maxArea);            if (height[left] >= height[right]) {                right--;            } else {                left++;            }        }        return maxArea;    }}
☑️ ⭐

LeetCode腾讯精选练习50题-062.不同路径

题目描述

  • 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
  • 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
  • 问总共有多少条不同的路径?
exampleinput  : m = 3, n = 2output : 3note   : 从左上角开始,总共有 3 条路径可以到达右下角。         1. 向右 -> 向下 -> 向下         2. 向下 -> 向下 -> 向右         3. 向下 -> 向右 -> 向下input  : m = 7, n = 3output : 28

解题思路

思路1 动态规划
  • 这是一个标准的动态规划问题,可以完成状态转移

  • 转移方程:dp[i][j] = dp[i-1][j] + dp[i][j-1]

  • 因为只能向右或向下移动,所以:

    • 对于第一行和第一列的所有格子,都有且仅有一条路径可以直达其位置
    • 对于非第一行或非第一列的格子,到达其位置的路径数 = 到达其上方格子的路径数+到达其左方格子的路径数
  • 绘制网格图后,可以通过举例测试确定上述规律

  • 时间复杂度:O(m x n)

  • 空间复杂度:O(m x n)

    思路2 组合数学

    从左上角到右下角的过程中,需要移动 m+n-2 次,其中有 m-1 次向下移动,n-1 次向右移动。
    因此路径的总数,就等于从 m+n-2 次移动中选择 m-1 次向下移动的方案数,即组合数:

C = (m + n - 2)! / (m - 1)! * (n - 1)!
因此直接计算出这个组合数即可。
化简可得:C = (m + n - 2) * (m + n - 3) * ··· * n / (m - 1)!

  • 时间复杂度:O(m)
  • 空间复杂度:O(1)

代码(Java)

思路1代码

public class Solution {    public int uniquePaths(int m, int n) {        int[][] pathNum = new int[m][n];        /*        for (int j = 0; j < n; j++) {            pathNum[0][j] = 1;        }        for (int i = 1; i < m; i++) {            pathNum[i][0] = 1;        }        for (int i = 1; i < m; i++) {            for (int j = 1; j < n; j++) {                pathNum[i][j] = pathNum[i - 1][j] + pathNum[i][j - 1];            }        }        */        for (int i = 0; i < m; i++) {            for (int j = 0; j < n; j++) {                if (i == 0 || j == 0) {                    pathNum[i][j] = 1;                } else {                    pathNum[i][j] = pathNum[i - 1][j] + pathNum[i][j - 1];                }            }        }        /* 打印动态规划得到的二维数组        for (int i = 0; i < m; i++) {            for (int j = 0; j < n; j++) {                System.out.print(pathNum[i][j] + "\t");            }            System.out.println();        }        */        return pathNum[m - 1][n - 1];    }}

思路2代码

public class Solution2 {    public int uniquePaths(int m, int n) {        long ans = 1;        for (int x = n, y = 1; y < m; x++, y++) {            // x和y同时前进 m - 2 次,刚好满足化简后的公式            ans = ans * x / y;        }        return (int) ans;    }}
☑️ ☆

Git报错Kex_exchange_identification

问题:

  • 本地项目push到github失败

    Push failed
    Kex_exchange_identification: Connection closed by remote hostConnection closed by 20.205.243.166 port 22
    Could not read from remote repository.
    Please make sure you have the correct access rightsand the repository exists.

  • 同样地,从github中clone、pull、fetch也报上述错误

解决方法

  • 一般情况下是以上错误都是因为挂着VPN导致端口冲突

  • 目前遇到两类情况,解决办法如下。

1 网络本身无vpn,使用ShadowsocksR等工具科学上网

  • 退出ShadowsocksR即可
  • 缺点:访问github会变慢,毕竟把vpn关掉了。如果开vpn本身就是为了更快速的访问github,那这样的操作就很费劲,每次和远程仓库交互都要关掉vpn,搞完再打开,推荐2.1。

2 网络本身挂载vpn,如openwrt上安装了ShadowsocksR

  • 解决办法有两种(推荐第一种)
2.1 修改项目目录中隐藏文件夹 .git 内的 config 文件
  • 将 Project/.git/config 文件中ssh格式的url,修改为github仓库中https格式的url。如:

    url = https://github.com/username/SpringBootWebTest.git

    image-20220921101237310

  • 因为开着vpn,代理端口走22;同时git的ssh一般也使用22端口,这样造成冲突;而git的https一般使用443端口,不会产生冲突。

  • 一般企业防火墙会打开80和443这两个http/https协议的端口,因此在架设了企业防火墙的时候使用https可以很好地绕开安全限制使用git;但是对于ssh来说,企业防火墙很可能没打开22端口。

  • 如果按以上操作修改之后报错Invocation failed Server returned invalid Response.,则到IDEA等软件的配置界面,选中 Use credential helper 即可,参考。路径如下:

    File -> Settings -> Version Control -> Git-> Use credential helper

    image-20220921102253464

2.2 修改openwrt的ShadowsocksR的访问控制配置
  • 在访问控制的不走代理名单中加入 github.com ,保存并应用,这样访问github的操作就与 1 中一样了,缺点也一样

其他

还有其他的不同情况下的解决方案,可以根据实际情况寻找对应的解决办法

☑️ ⭐

LeetCode腾讯精选练习50题-054.螺旋矩阵

题目描述

  • 给定一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
exampleinput  : matrix = [[1,2,3],[4,5,6],[7,8,9]]output : [1,2,3,6,9,8,7,4,5]input  : matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]output : [1,2,3,4,8,12,11,10,9,5,6,7]

解题思路 按层模拟

  • 按照题目要求的转圈遍历顺序,使用上下左右四个界限坐标来标记每圈的位置,模拟整个向内环绕的元素获取过程。
  • 时间复杂度:O(m×n)
  • 空间复杂度:O(1)

代码(Java)

public class Solution {    public List<Integer> spiralOrder(int[][] matrix) {        List<Integer> list = new ArrayList<>();        int length = matrix.length;        int width = matrix[0].length;        int left = 0, right = width - 1, top = 0, bottom = length - 1;        while (left <= right && top <= bottom) {            for (int i = left; i <= right; i++) {                list.add(matrix[top][i]);            }            for (int j = top + 1; j <= bottom; j++) {                list.add(matrix[j][right]);            }            if (left < right && top < bottom) {                for (int p = right - 1; p >= left; p--) {                    list.add(matrix[bottom][p]);                }                for (int q = bottom - 1; q > top; q--) {                    list.add(matrix[q][left]);                }            }            left++;            right--;            top++;            bottom--;        }        return list;    }}
☑️ ☆

LeetCode腾讯精选练习50题-059.螺旋矩阵II

题目描述

  • 给定一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
exampleinput  : n = 3output : [[1,2,3],[8,9,4],[7,6,5]]input  : n = 1output : [[1]]

解题思路 按层模拟

  • 按照题目要求的转圈填充顺序,使用上下左右四个界限坐标来标记每圈的位置,模拟整个向内环绕的填充过程。
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)

代码(Java)

public class Solution {    public int[][] generateMatrix(int n) {        int left = 0, right = n - 1, top = 0, bottom = n - 1;        int[][] matrix = new int[n][n];        int c = 1;        while (left <= right && top <= bottom) {            for (int i = left; i <= right; i++) {                matrix[top][i] = c++;            }            for (int j = top + 1; j <= bottom; j++) {                matrix[j][right] = c++;            }            for (int p = right - 1; p >= left; p--) {                matrix[bottom][p] = c++;            }            for (int q = bottom - 1; q > top; q--) {                matrix[q][left] = c++;            }            left++;            right--;            top++;            bottom--;        }        return matrix;    }}
☑️ ⭐

LeetCode腾讯精选练习50题-142.环形链表 II

题目描述

  • 给定一个链表的头节点 head ,返回链表开始入环的第一个节点。
  • 如果链表无环,则返回 null。
  • 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
  • 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。
  • 如果 pos 是 -1,则在该链表中没有环。
  • 注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
  • 不允许修改 链表。
exampleinput  : head = [3,2,0,-4], pos = 1output : tail connects to node index 1note   : 返回索引为 1 的链表节点,链表中有一个环,其尾部连接到第二个节点。input  : head = [1,2], pos = 0output : tail connects to node index 0note   : 返回索引为 0 的链表节点,链表中有一个环,其尾部连接到第二个节点。input  : head = [1], pos = -1output : no cyclenote   : 返回 null,链表中没有环。

解题思路

思路1 快慢指针
  • 设快指针是fast,慢指针是slow,两者起点相同
  • 快指针以2倍速前进,慢指针以逐步前进;可知fast前进的路程s1是slow前进的路程s2的两倍,即 s1 = 2 * s2
  • 设从起点到入环节点的距离(节点个数)是a,入环节点顺序到fast和slow相遇节点的距离为b,相遇节点再顺序到入环节点的距离为c,则有
    • 环长(环内节点总数)为:s3 = b + c
    • fast走过的路程:s1 = a + n(b + c) + b
    • slow走过的路程:s2 = a + b
    • 以上则有:s1 = 2 * s2 => a + n(b + c) + b = 2(a + b)
      • 移项可得:a = c + (n - 1)(b + c),即,a = c + (n - 1)s3 (⭐)
      • 由⭐可知,从链表起点出发到入环节点的距离a,等于从相遇节点出发向入环节点移动 n - 1 圈再走一个从相遇节点到入环节点的距离
      • 即,两个指针,一个从起始节点,另一个从相遇节点,同时出发,他们最终会经过相等数量的节点,在入环节点处相遇,其中第一个节点一直在环外走,第二个节点一直在环内走
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)
思路2 哈希表
  • 遍历链表,把遍历过的节点存入哈希表中
  • 当第一次出现被遍历到的节点已经存在于哈希表中的情况时,这个节点就是环形的入口节点
  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

代码(Java)

思路1代码

public class Solution1 {    public ListNode detectCycle(ListNode head) {        if (head == null || head.next == null) {            return null;        }        ListNode slow = head, fast = head;        while (fast != null) {            slow = slow.next;            if (fast.next != null) {                fast = fast.next.next;            } else {                return null;            }            if (fast == slow) {                ListNode p = head;                while (p != slow) {                    p = p.next;                    slow = slow.next;                }                return p;            }        }        return null;    }}

思路2代码

public class Solution2 {    public ListNode detectCycle(ListNode head) {        if (head == null || head.next == null) {            return null;        }        Set<ListNode> set = new LinkedHashSet<>();        ListNode slow = head;        while (slow != null){            if (set.contains(slow)){                return slow;            }            set.add(slow);            slow = slow.next;        }        return null;    }}
🔲 ⭐

LeetCode腾讯精选练习50题-061.旋转链表

题目描述

  • 给定一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
exampleinput  : head = [1,2,3,4,5], k = 2output : [4,5,1,2,3]input  : head = [0,1,2], k = 4output : [2,0,1]

解题思路

  • 如果满足以下条件,可以直接返回原链表
    • 链表为空,即 head = null
    • 链表只有一个节点,即 head.next = null
    • 旋转次数 k 为0
    • 旋转次数 k 是链表长度的倍数,即旋转后的链表还是原样
思路1 双指针/快慢指针
  • 第一次遍历获取链表长度

  • 用快慢指针拉开遍历差距,并借助慢指针获取结果链表的起始节点,即

    • 先考虑简单情况 k 严格小于 链表长度 n,那么就是要找到原链表的倒数第k个节点作为结果链表的起始节点
    • 再拓展到k > n的情况,此时只需要刷新k,即令 k = k % n,转变成第一种简单情况即可
  • 使链表闭合,并断开新的起始节点与其之前节点的链接

  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

    思路2 双指针的反向思考
  • 其实就是双指针思路的变形

  • 第一次遍历获取链表长

  • 反向思考旋转后结果链表起始节点的位置,使用单个指针遍历来获取结果链表的起始节点,即

    • 先考虑简单情况 k 严格小于 链表长度 n,那么就是要找到原链表的倒数第k个节点原始链表的正数第 n - k 个节点的后继结点作为结果链表的起始节点
    • 再拓展到k > n 的情况,此时只需要将 k % n,转变成第一种简单情况即可,即刷新k,令 k = n - k % n
  • 使链表闭合,并断开新的起始节点与其之前节点的链接

  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

代码(Java)

思路1代码

public class Solution1 {    public ListNode rotateRight(ListNode head, int k) {        if (k == 0 || head == null || head.next == null) {            return head;        }        ListNode iter = head;        int len = 0;        ListNode tail = null;        while (iter != null) {            if (iter.next == null) {                tail = iter;            }            len++;            iter = iter.next;        }        // 如果旋转次数恰好是链表长度的倍数,说明旋转后也是原样,所以不需要移动        if (k % len == 0) {            return head;        }        // 初始令计数器延后一位(即-1),以获取结果链表的起始位置的前一个位置        // 此处若初始令计数器为0,则获得的是结果链表的其实位置,无法将它与之前的节点断开        int cnt = -1;        // 先令快指针向前遍历k%len步        ListNode fast = head;        while (cnt != k % len) {            cnt++;            fast = fast.next;        }        // 再同步开启慢指针的遍历        // 当快指针为空时,说明慢指针已经到达了目标结果链表的起始节点的前一个位置        ListNode second = head;        while (fast != null) {            fast = fast.next;            second = second.next;        }        // 使原始链表闭合为环        tail.next = head;        // 获得新起点        ListNode res = second.next;        // 从新起点之前断开        second.next = null;        // 返回结果        return res;    }}

思路2代码

public class Solution2 {    public ListNode rotateRight2(ListNode head, int k) {        if (k == 0 || head == null || head.next == null) {            return head;        }        ListNode first = head;        int len = 1;        while (first.next != null) {            len++;            first = first.next;        }        if (k % len == 0) {            return head;        }        // 第二次遍历        ListNode second = head;        int cnt = 1;        // 移动k个位置,其实就是以 第len - k % len个节点的后继节点 作为新链表的头节点,并使其与其之前的节点断开链接        // 所以此处可以做减法来替代快慢指针的同步遍历,只需要遍历一次即可。        int step = len - k % len;        while (cnt < step) {            cnt++;            second = second.next;        }        // 使之闭合为环        first.next = head;        ListNode res = second.next;        second.next = null;        return res;    }}
☑️ ☆

LeetCode腾讯精选练习50题-016.最接近的三数之和

题目描述

  • 给定一个长度为 n 的整数数组 nums 和 一个目标值 target。
  • 请从 nums 中选出三个整数,使它们的和与 target 最接近。
  • 返回这三个数的和。
  • 假定每组输入只存在恰好一个解。
exampleinput  : nums = [-1,2,1,-4], target = 1output : 2note   : 与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。input  : nums = [0,0,0], target = 1output : 0

解题思路 排序+双指针

  • 先把数组排序

  • 外层循环从头开始遍历,逐个固定遍历到的元素作为三数的第1个数,然后内层循环是在其后方区间使用首尾双指针

  • 根据三数之和确定移动首部指针还是尾部指针

  • 对比每组三数之和tmpSumtarget差值绝对值,不断保留差值绝对值最小的三数之和

  • 最终保留下来的结果就是与target差值绝对值最小的三数之和,即最接近target的三数之和

  • 时间复杂度:O(n^2)

  • 空间复杂度:O(1)

    代码(Java)

    public class Solution {    // 推荐参考下方的写法2    public int threeSumClosest(int[] nums, int target) {        Arrays.sort(nums);        if (nums.length == 3) {            return nums[0] + nums[1] + nums[2];        }        int n = nums.length;        int bigger = Integer.MAX_VALUE;        int smaller = Integer.MIN_VALUE;        for (int i = 0; i < n - 2; i++) {            if (i > 0 && nums[i] == nums[i - 1]) {                continue;            }            int j = i + 1;            int k = n - 1;            while (j < k) {                int tmpSum = nums[i] + nums[j] + nums[k];                if (tmpSum > target) {                    if (tmpSum <= bigger) {                        bigger = tmpSum;                    }                    k--;                } else if (tmpSum < target) {                    if (tmpSum >= smaller) {                        smaller = tmpSum;                    }                    j++;                } else {                    return target;                }            }        }        if (bigger == Integer.MAX_VALUE) {            return smaller;        }        if (smaller == Integer.MIN_VALUE) {            return bigger;        }        int left = target - smaller;        int right = bigger - target;        if (left > right) {            return bigger;        } else {            return smaller;        }    }    // 以下为与思路描述更加一一对应的写法    // 相较于写法1更易读    public int threeSumClosest2(int[] nums, int target) {        Arrays.sort(nums);        int n = nums.length;        int best = 10001;        for (int i = 0; i < n; i++) {            if (i > 0 && nums[i] == nums[i - 1]) {                continue;            }            int j = i + 1;            int k = n - 1;            while (j < k) {                int tmpSum = nums[i] + nums[j] + nums[k];                int cur = Math.abs(tmpSum - target);                int old = Math.abs(best - target);                if (cur < old) {                    best = tmpSum;                }                if (tmpSum > target) {                    k--;                } else if (tmpSum < target) {                    j++;                } else {                    return target;                }            }        }        return best;    }}
☑️ ☆

LeetCode腾讯精选练习50题-015.三数之和

题目描述

  • 给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?
  • 请找出所有和为 0 且不重复的三元组。
exampleinput  : nums = [-1,0,1,2,-1,-4]output : [[-1,-1,2],[-1,0,1]]input  : nums = []output : []input  : nums = [0]output : []

解题思路

思路1 双指针
  • 排序

  • 外层循环从头开始遍历,逐个固定遍历到的元素作为三数的第1个数,然后内层循环是在其后方区间使用首尾双指针

  • 根据三数之和确定移动首部指针还是尾部指针

  • 移动指针的过程中遇到相同则跳过,以避免重复解

  • 遇到符合要求的三数之和则将其记录到结果列表中

  • 执行用时:17 ms, 在所有 Java 提交中击败了99.71%的用户

  • 内存消耗:45 MB, 在所有 Java 提交中击败了92.86%的用户

  • 时间复杂度:O(n^2)

  • 空间复杂度:O(n))

    思路2 Map
  • 排序

  • 外层循环从中部(1 ~ n-1)开始遍历,逐个固定遍历到的元素作为三数的第2个数,内层循环是在其前后两个小的区间内依次顺序遍历,以确定其他两个元素

  • 内层循环中,对当前第2个数的前方区间的遍历,记录所有第1个数和已经固定的第2个数的求和组合

  • 然后继续在内层循环中,遍历当前第2个数的后方区间,找到map中是否存在某个键,等于当前元素的相反数

    • 若存在,则说明当前找到的三个数符合要求,将它们记录到结果列表中
    • 额外再使用一个map来记录前两个数组成的map和第三个数
  • 两层循环的遍历过程中遇到相同则跳过,以避免重复解

  • 执行用时: 266 ms

  • 内存消耗: 46.9 MB

  • 时间复杂度:O(n^2)

  • 空间复杂度:O(n))

代码(Java)

思路1代码

public class Solution1 {    public List<List<Integer>> threeSum(int[] nums) {        // 排序        Arrays.sort(nums);        int n = nums.length;        List<List<Integer>> lists = new ArrayList<>(256);        if (n < 3 || nums[0] > 0 || nums[n - 1] < 0) {            return lists;        }        // 转换成 a + b = -c        int i, j, k;        for (i = 0; i < n - 2; i++) {            if (nums[i] > 0) {                break;            }            if (i > 0 && nums[i] == nums[i - 1]) {                continue;            }            j = i + 1;            k = n - 1;            while (j < k) {                int target = nums[j] + nums[k];                if (target < -nums[i]) {                    j++;                } else if (target > -nums[i]) {                    k--;                } else {                    lists.add(Arrays.asList(new Integer[]{nums[i], nums[j], nums[k]}));                    k--;                    j++;                    while (j < k && nums[j] == nums[j - 1]) {                        j++;                    }                    while (j < k && nums[k] == nums[k + 1]) {                        k--;                    }                }            }        }        return lists;    }}

思路2代码

public class Solution2 {    public List<List<Integer>> threeSum(int[] nums) {        // 排序        Arrays.sort(nums);        int n = nums.length;        List<List<Integer>> lists = new ArrayList<>(256);        if (n < 3 || nums[0] > 0 || nums[n - 1] < 0) {            return lists;        }        // 转换成 a + b = -c        int i, j, k;        Map<Integer, Integer> map = new HashMap<>();        Map<Integer, Map<Integer, Integer>> ansHash = new HashMap<>();        for (i = 1; i < n - 1; i++) {            // i是中间元素,j是从0到i找元素,k是从i到n找元素            // 即借助中间元素来缩小两侧的查找范围            map.clear();            for (j = 0; j < i; j++) {                int x = nums[i] + nums[j];                map.put(x, 1);            }            for (k = i + 1; k < n; k++) {                if (k > i + 1 && nums[k] == nums[k - 1]) {                    continue;                }                if (map.containsKey(-nums[k])) {                    int b = nums[i];                    int c = nums[k];                    int a = -(nums[i] + nums[k]);                    if (!ansHash.containsKey(a) || !ansHash.get(a).containsKey(b)) {                        ansHash.put(a, new HashMap<>());                        ansHash.get(a).put(b, 1);                        lists.add(Arrays.asList(new Integer[] {a, b, c}));                    }                }            }        }        return lists;    }}

思路3代码(思路2代码的改写,修改了map的使用方式)

public class Solution3 {    public List<List<Integer>> threeSum(int[] nums) {        // 排序        Arrays.sort(nums);        int n = nums.length;        List<List<Integer>> lists = new ArrayList<>(256);        if (n < 3 || nums[0] > 0 || nums[n - 1] < 0) {            return lists;        }        // 转换成 a + b = -c        int i, j, k;        Map<Integer, Integer> map = new HashMap<>();        Map<Map<Integer, Integer>, Integer> ansHash = new HashMap<>();        for (j = 1; j < n - 1; j++) {            // i是中间元素,j是从0到i找元素,k是从i到n找元素            // 即借助中间元素来缩小两侧的查找范围            map.clear();            for (i = 0; i < j; i++) {                int x = nums[i] + nums[j];                map.put(x, 1);            }            for (k = j + 1; k < n; k++) {                if (k > j + 1 && nums[k] == nums[k - 1]) {                    continue;                }                if (map.containsKey(-nums[k])) {                    int a = -(nums[j] + nums[k]);                    int b = nums[j];                    int c = nums[k];                    Map<Integer, Integer> t = new HashMap<>();                    t.put(a, b);                    if (!ansHash.containsKey(t)) {                        ansHash.put(t, 1);                        lists.add(Arrays.asList(new Integer[]{a, b, c}));                    }                }            }        }        return lists;    }}
☑️ ☆

LeetCode腾讯精选练习50题-008.字符串转换整数 (atoi)

题目描述

  • 实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。
  • 函数 myAtoi(string s) 的算法如下:
  • 读入字符串并丢弃无用的前导空格
  • 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。
  • 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。
  • 将前面步骤读入的这些数字转换为整数(即,”123” -> 123, “0032” -> 32)。如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。
  • 如果整数数超过 32 位有符号整数范围 [−2^31, 2^(31 − 1)] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −2^31 的整数应该被固定为 −2^31 ,大于 2^(31 − 1) 的整数应该被固定为 2^(31 − 1) 。
  • 返回整数作为最终结果。
  • 注意:
  • 本题中的空白字符只包括空格字符 ‘ ‘ 。
  • 除前导空格或数字后的其余字符串外,请勿忽略 任何其他字符。
exampleinput  : s = "   -42"output : -42input  : s = "20000000000000000000"output : 2147483647input  : s = "  0000000000012345678"output : 12345678input  : s = "    0000000000000   "output : 0

解题思路

  • 题目要求中已经明确了所有思路。
  • 代码中不使用Long来提前处理数据,只使用int以符合题意,因此就要求在遍历过程中对越界问题提前做出判断
  • 无非是直接在遍历过程中直接更新数值或者借助字符串截取一下再处理数值
    • 直接更新数值
      • 时间复杂度:O(n)
      • 空间复杂度:O(1)
    • 借助字符串截取后再处理
      • 时间复杂度:O(n)
      • 空间复杂度:O(n)

代码(Java)

public class Solution {    public int myAtoi(String s) {        if (s == null) {            return 0;        }        // 读入字符串并丢弃无用的前导空格        s = s.trim();        if (s.length() == 0) {            return 0;        }        // 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。        // 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。        boolean flag = false;        int i = 0;        if (s.charAt(i) == '-') {            flag = true;            i++;        } else if (s.charAt(i) == '+') {            i++;        }        // 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。        // 字符串的其余部分将被忽略。        // 将前面步骤读入的这些数字转换为整数(即,"123" -> 123, "0032" -> 32)。        // 如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。        int res = 0;        for (; i < s.length() && s.charAt(i) >= '0' && s.charAt(i) <= '9'; i++) {            // 如果整数数超过 32 位有符号整数范围 [−2^31,  2^(31 − 1)] ,需要截断这个整数,使其保持在这个范围内。            // 具体来说,小于 −231 的整数应该被固定为 −2^31 ,大于 2^(31 − 1) 的整数应该被固定为 2^(31 − 1) 。            if (!flag) {                if (res > Integer.MAX_VALUE / 10) {                    return Integer.MAX_VALUE;                } else if (res == Integer.MAX_VALUE / 10) {                    if (s.charAt(i) >= '7') {                        return Integer.MAX_VALUE;                    }                }            }            if (flag) {                if (-res < Integer.MIN_VALUE / 10) {                    return Integer.MIN_VALUE;                } else if (-res == Integer.MIN_VALUE / 10) {                    if (s.charAt(i) >= '8') {                        return Integer.MIN_VALUE;                    }                }            }            res = res * 10 + Integer.parseInt(String.valueOf(s.charAt(i)));        }        return flag ? -res : res;    }    public int myAtoi2(String s) {        if (s == null) {            return 0;        }        // 读入字符串并丢弃无用的前导空格        s = s.trim();        if (s.length() == 0) {            return 0;        }        // 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。        // 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。        boolean flag = false;        int i = 0;        if (s.charAt(i) == '-') {            flag = true;            i++;        } else if (s.charAt(i) == '+') {            i++;        }        // 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。        // 字符串的其余部分将被忽略。        // 将前面步骤读入的这些数字转换为整数(即,"123" -> 123, "0032" -> 32)。        // 如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。        while (i < s.length() && s.charAt(i) == '0') {            i++;        }        int j = i;        while (i < s.length() && s.charAt(i) >= '0' && s.charAt(i) <= '9') {            i++;        }        if (i == j) {            return 0;        }        // 如果整数数超过 32 位有符号整数范围 [−2^31,  2^(31 − 1)] ,需要截断这个整数,使其保持在这个范围内。        // 具体来说,小于 −231 的整数应该被固定为 −2^31 ,大于 2^(31 − 1) 的整数应该被固定为 2^(31 − 1) 。        String tmp = s.substring(j, i);        if (tmp.length() == 10) {            tmp = tmp.substring(0, 10);            if (!flag && tmp.compareTo("2147483647") >= 0) {                return Integer.MAX_VALUE;            }            if (flag && tmp.compareTo("2147483648") >= 0) {                return Integer.MIN_VALUE;            }        }        if (tmp.length() > 10) {            if (!flag) {                return Integer.MAX_VALUE;            } else {                return Integer.MIN_VALUE;            }        }        int res = Integer.parseInt(tmp);        return flag ? -res : res;    }}
🔲 ☆

LeetCode腾讯精选练习50题-007.整数反转

题目描述

  • 给定一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。
  • 如果反转后整数超过 32 位的有符号整数的范围 [−2^31, 2^(31 − 1)] ,就返回 0。
  • 假设环境不允许存储 64 位整数(有符号或无符号)。
exampleinput  : x = -123output : -321input  : x = 120output : 21input  : x = -2147483648output : 0input  : x = 1534236469output : 0

解题思路

  • 因为输入数据保证在 32 位的有符号整数的范围内,所以不需要考虑像 8463847412 这种超出本身就超出范围且反转后依然超出范围的情况

    思路1 字符串
  • 借助字符串str接收 x ,然后反转字符串str,并判断输入 x 的正负性、str与 “2147483648”、”-2147483648”的大小关系

    • 若x为正(+),且字符串str大于字符串”2147483647”,超出上界,返回0
    • 若x为负(-),且字符串str大于字符串”2147483648”,超出下界,返回0
    • 正常则取其Integer值并添上符号返回
  • 时间复杂度:O(n)

  • 空间复杂度:O(n),使用了额外的等“长”字符串

    思路2 让步比大小
  • “让步”的意思是在数值超出32位整形范围之前就把它判断出来,即用最大值/10和最小值/10的范围作为临时上下界来防止完全反转后的越界

  • 先统一转换成正数

    • 用整形变量res(初始值为0)不断扩大十倍并加上当前x%10得到的数,然后将x/10去掉它的末尾数值
    • 在执行以上操作之前,先判断一下当前的res是否大于32位整型变量的最大值Integer.MAX_VALUE/10,若是则说明完全的反转后肯定会越界,不必继续操作,直接按要求返回0即可
    • 当x减小到0的时候,判断原始的符号位,并给结果res添加上,然后返回
  • 不转换正数,统一判断

    • 用整形变量res(初始值为0)不断扩大十倍并加上当前x%10得到的数,然后将x/10去掉它的末尾数值
    • 在执行以上操作之前,先判断一下以下两条,若满足则说明完全的反转后肯定会越界,不必继续操作,直接按要求返回0即可
      • 当前的res是否大于32位整型变量的最大值Integer.MAX_VALUE / 10
      • 当前的res是否小于32位整型变量的最小值Integer.MIN_VALUE / 10
    • 当x等于0的时候,返回res即可(不需要在判断符号位了)
  • 时间复杂度:O(log(n))

  • 空间复杂度:O(1),只使用了有限个整型变量

代码(Java)

思路1代码

public class Solution1 {    public int reverse(int x) {        if (x == Integer.MIN_VALUE){            // 因为输入保证为32位整数,所以不需要考虑更小的数值,判断到Integer的最小值即可            return 0;        }        boolean flag = false;        if (x < 0) {            flag = true;            x = -x;        }        StringBuilder s = new StringBuilder(x + "");        String str = s.reverse().toString();        if (s.length() >= 10) {            if (!flag && str.compareTo("2147483647") > 0) {                return 0;            }            if (flag && str.compareTo("2147483648") > 0) {                return 0;            }        }        return flag ? -Integer.parseInt(str) : Integer.parseInt(str);    }}

思路2代码

public class Solution2 {    public int reverse(int x) {        if (x < 0 && x == -x) {            return 0;        }        boolean flag = false;        if (x < 0) {            flag = true;            x = -x;        }        int y = 0;        while (x > 0) {            if (y > Integer.MAX_VALUE / 10) {                return 0;            }            y = y * 10 + x % 10;            x /= 10;        }        return flag ? -y : y;    }    // 以下为简化代码    public int reverse2(int x) {        int y = 0;        int max = Integer.MAX_VALUE / 10;        int min = Integer.MIN_VALUE / 10;        while (x != 0) {            if (y > max || y < min) {                return 0;            }            y = y * 10 + x % 10;            x /= 10;        }        return y;    }}
❌