独书先生 Menu

LeetCode刷题笔记:数组中重复的数据

问题

给你一个长度为 n 的整数数组 nums ,其中 nums 的所有整数都在范围 [1, n] 内,且每个整数出现 一次两次 。请你找出所有出现 两次 的整数,并以数组形式返回。

你必须设计并实现一个时间复杂度为 O(n) 且仅使用常量额外空间的算法解决此问题。

示例 1:

输入:nums = [4,3,2,7,8,2,3,1]

输出:[2,3]

示例 2:

输入:nums = [1,1,2]

输出:[1]

示例 3:

输入:nums = [1]

输出:[]

提示:

  • n == nums.length
  • 1 <= n <= 105
  • 1 <= nums[i] <= n
  • nums 中的每个元素出现 一次两次

解法一

思路:

利用 Set 值唯一的特性,不断向一个空的 Set 里面添加 nums 中的数字,再使用 set.add方法,通过获取 set 长度是否增加来判断是否有重复数字出现。

代码:

/**
 * @param {number[]} nums
 * @return {number[]}
 */
var findDuplicates = function(nums) {
    const set = new Set() // 唯一值检验
    const result = [] // 结果数组

    nums.forEach(n => {
        const preSize = set.size

        // 使用 set.add 方法,通过获取 set 长度是否增加来判断是否有重复数字出现
        set.add(n)

        // 发现重复数字
        if(preSize === set.size){
            result.push(n)
        }
    })

    return result
};

解法二

思路:

遍历整个数组,将每一个数字视为数组位置信息,再将每一个位置对应的数字反转为负数,相当于做一个标识,表明这个数字对应的位置,已经有数字占用了,下一次再遇到这个数字如果发现是负数就表明已经出现过。

比如 [4,3,2,7,8,2,3,1],走到第一个 2 的时候,翻转位置为 1 的数字 3-3,走到下一个 2 的时候,就能发现位置为 1 的数字为 -3, 已经被翻转过了,表明数字 2 出现了两次。

代码:

/**
 * @param {number[]} nums
 * @return {number[]}
 */
var findDuplicates = function(nums) {
    let result = [];
    for (let i = 0; i < nums.length; i++) {
        let num = Math.abs(nums[i]);
        if (nums[num - 1] > 0) {
            /**
             把数字翻转为负数的目的是,做一个标识,表明这个数字对应的位置,已经有数字占用了,下一次再遇到这个数字如果发现是负数就表明已经出现过

             比如[4,3,2,7,8,2,3,1]

             走到第一个2的时候,位置为1的数字为3,将3翻转为-3,走到下一个2的时候,翻转3的时候发现已经被翻转过了
             */
            nums[num - 1] *= -1;
        } else {
            result.push(num);
        }
    }
    return result;

};

参考

如何在 JavaScript 中实现事件总线 (Event Bus)

原文:https://dushusir.com/js-event-bus/

介绍

Event Bus 事件总线,通常作为多个模块间的通信机制,相当于一个事件管理中心,一个模块发送消息,其它模块接受消息,就达到了通信的作用。

比如,Vue 组件间的数据传递可以使用一个 Event Bus 来通信,也可以用作微内核插件系统中的插件和核心通信。

原理

Event Bus 本质上是采用了发布-订阅的设计模式,比如多个模块 ABC 订阅了一个事件 EventX,然后某一个模块 X 在事件总线发布了这个事件,那么事件总线会负责通知所有订阅者 ABC,它们都能收到这个通知消息,同时还可以传递参数。

// 关系图
                           模块X
                            ⬇发布EventX
╔════════════════════════════════════════════════════════════════════╗
║                         Event Bus                                  ║
║                                                                    ║
║         【EventX】       【EventY】       【EventZ】   ...           ║
╚════════════════════════════════════════════════════════════════════╝
  ⬆订阅EventX            ⬆订阅EventX           ⬆订阅EventX
 模块A                   模块B                  模块C

分析

如何使用 JavaScript 来实现一个简单版本的 Event Bus

  • 首先构造一个 EventBus 类,初始化一个空对象用于存放所有的事件
  • 在接受订阅时,将事件名称作为 key 值,将需要在接受发布消息后执行的回调函数作为 value 值,由于一个事件可能有多个订阅者,所以这里的回调函数要存储成列表
  • 在发布事件消息时,从事件列表里取得指定的事件名称对应的所有回调函数,依次触发执行即可

以下是代码详细实现,可以复制到谷歌浏览器控制台直接运行检测效果。

代码

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
  }
  // 发布事件
  publish(eventName) {
    // 取出当前事件所有的回调函数
    const callbackList = this.eventObject[eventName];

    if (!callbackList) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let callback of callbackList) {
      callback();
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      this.eventObject[eventName] = [];
    }

    // 存储订阅者的回调函数
    this.eventObject[eventName].push(callback);
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", () => {
  console.log("模块A");
});
eventBus.subscribe("eventX", () => {
  console.log("模块B");
});
eventBus.subscribe("eventX", () => {
  console.log("模块C");
});

// 发布事件eventX
eventBus.publish("eventX");

// 输出
> 模块A
> 模块B
> 模块C

上面我们实现了最基础的发布和订阅功能,实际应用中,还可能有更进阶的需求。

进阶

1. 如何在发送消息时传递参数

发布者传入一个参数到 EventBus 中,在 callback 回调函数执行的时候接着传出参数,这样每一个订阅者就可以收到参数了。

代码

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
  }
  // 发布事件
  publish(eventName, ...args) {
    // 取出当前事件所有的回调函数
    const callbackList = this.eventObject[eventName];

    if (!callbackList) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let callback of callbackList) {
      // 执行时传入参数
      callback(...args);
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      this.eventObject[eventName] = [];
    }

    // 存储订阅者的回调函数
    this.eventObject[eventName].push(callback);
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块A", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块B", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);


// 输出
> 模块A {msg: 'EventX published!'} 1
> 模块B {msg: 'EventX published!'} 1
> 模块C {msg: 'EventX published!'} 1

2. 订阅后如何取消订阅

有时候订阅者只想在某一个时间段订阅消息,这就涉及带取消订阅功能。我们将对代码进行改造。

首先,要实现指定订阅者取消订阅,每一次订阅事件时,都生成唯一一个取消订阅的函数,用户直接调用这个函数,我们就把当前订阅的回调函数删除。

// 每一次订阅事件,都生成唯一一个取消订阅的函数
const unSubscribe = () => {
  // 清除这个订阅者的回调函数
  delete this.eventObject[eventName][id];
};

其次,订阅的回调函数列表使换成对象结构存储,为每一个回调函数设定一个唯一 id, 注销回调函数的时候可以提高删除的效率,如果还是使用数组的话需要使用 split 删除,效率不如对象的 delete

代码

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
    // 回调函数列表的id
    this.callbackId = 0;
  }
  // 发布事件
  publish(eventName, ...args) {
    // 取出当前事件所有的回调函数
    const callbackObject = this.eventObject[eventName];

    if (!callbackObject) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let id in callbackObject) {
      // 执行时传入参数
      callbackObject[id](...args);
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    const id = this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块A", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块B", obj, num);
});
const subscriberC = eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);

// 模块C取消订阅
subscriberC.unSubscribe();

// 再次发布事件eventX,模块C不会再收到消息了
eventBus.publish("eventX", { msg: "EventX published again!" }, 2);

// 输出
> 模块A {msg: 'EventX published!'} 1
> 模块B {msg: 'EventX published!'} 1
> 模块C {msg: 'EventX published!'} 1
> 模块A {msg: 'EventX published again!'} 2
> 模块B {msg: 'EventX published again!'} 2

3. 如何只订阅一次

如果一个事件只发生一次,通常也只需要订阅一次,收到消息后就不用再接受消息。

首先,我们提供一个 subscribeOnce 的接口,内部实现几乎和 subscribe 一样,只有一个地方有区别,在 callbackId 前面的加一个字符 d,用来标示这是一个需要删除的订阅。

// 标示为只订阅一次的回调函数
const id = "d" + this.callbackId++;

然后,在执行回调函数后判断当前回调函数的 id 有没有标示,决定我们是否需要删除这个回调函数。

// 只订阅一次的回调函数需要删除
if (id[0] === "d") {
  delete callbackObject[id];
}

代码

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
    // 回调函数列表的id
    this.callbackId = 0;
  }
  // 发布事件
  publish(eventName, ...args) {
    // 取出当前事件所有的回调函数
    const callbackObject = this.eventObject[eventName];

    if (!callbackObject) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let id in callbackObject) {
      // 执行时传入参数
      callbackObject[id](...args);

      // 只订阅一次的回调函数需要删除
      if (id[0] === "d") {
        delete callbackObject[id];
      }
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    const id = this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 只订阅一次
  subscribeOnce(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    // 标示为只订阅一次的回调函数
    const id = "d" + this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块A", obj, num);
});
eventBus.subscribeOnce("eventX", (obj, num) => {
  console.log("模块B", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);

// 再次发布事件eventX,模块B只订阅了一次,不会再收到消息了
eventBus.publish("eventX", { msg: "EventX published again!" }, 2);

// 输出
> 模块A {msg: 'EventX published!'} 1
> 模块C {msg: 'EventX published!'} 1
> 模块B {msg: 'EventX published!'} 1
> 模块A {msg: 'EventX published again!'} 2
> 模块C {msg: 'EventX published again!'} 2

4. 如何清除某个事件或者所有事件

我们还希望通过一个 clear 的操作来将指定事件的所有订阅清除掉,这个通常在一些组件或者模块卸载的时候用到。

  // 清除事件
  clear(eventName) {
    // 未提供事件名称,默认清除所有事件
    if (!eventName) {
      this.eventObject = {};
      return;
    }

    // 清除指定事件
    delete this.eventObject[eventName];
  }

和取消订阅的逻辑相似,只不过这里统一处理了。

代码

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
    // 回调函数列表的id
    this.callbackId = 0;
  }
  // 发布事件
  publish(eventName, ...args) {
    // 取出当前事件所有的回调函数
    const callbackObject = this.eventObject[eventName];

    if (!callbackObject) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let id in callbackObject) {
      // 执行时传入参数
      callbackObject[id](...args);

      // 只订阅一次的回调函数需要删除
      if (id[0] === "d") {
        delete callbackObject[id];
      }
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    const id = this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 只订阅一次
  subscribeOnce(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    // 标示为只订阅一次的回调函数
    const id = "d" + this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 清除事件
  clear(eventName) {
    // 未提供事件名称,默认清除所有事件
    if (!eventName) {
      this.eventObject = {};
      return;
    }

    // 清除指定事件
    delete this.eventObject[eventName];
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块A", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块B", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);

// 清除
eventBus.clear("eventX");

// 再次发布事件eventX,由于已经清除,所有模块都不会再收到消息了
eventBus.publish("eventX", { msg: "EventX published again!" }, 2);

// 输出
> 模块A {msg: 'EventX published!'} 1
> 模块B {msg: 'EventX published!'} 1
> 模块C {msg: 'EventX published!'} 1
> eventX not found!

5. TypeScript 版本

鉴于现在 TypeScript 已经被大规模采用,尤其是大型前端项目,我们简要的改造为一个 TypeScript 版本

可以复制以下代码到 TypeScript Playground 体验运行效果

代码

interface ICallbackList {
  [id: string]: Function;
}

interface IEventObject {
  [eventName: string]: ICallbackList;
}

interface ISubscribe {
  unSubscribe: () => void;
}

interface IEventBus {
  publish<T extends any[]>(eventName: string, ...args: T): void;
  subscribe(eventName: string, callback: Function): ISubscribe;
  subscribeOnce(eventName: string, callback: Function): ISubscribe;
  clear(eventName: string): void;
}

class EventBus implements IEventBus {
  private _eventObject: IEventObject;
  private _callbackId: number;
  constructor() {
    // 初始化事件列表
    this._eventObject = {};
    // 回调函数列表的id
    this._callbackId = 0;
  }
  // 发布事件
  publish<T extends any[]>(eventName: string, ...args: T): void {
    // 取出当前事件所有的回调函数
    const callbackObject = this._eventObject[eventName];

    if (!callbackObject) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let id in callbackObject) {
      // 执行时传入参数
      callbackObject[id](...args);

      // 只订阅一次的回调函数需要删除
      if (id[0] === "d") {
        delete callbackObject[id];
      }
    }
  }
  // 订阅事件
  subscribe(eventName: string, callback: Function): ISubscribe {
    // 初始化这个事件
    if (!this._eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this._eventObject[eventName] = {};
    }

    const id = this._callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this._eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this._eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this._eventObject[eventName]).length === 0) {
        delete this._eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 只订阅一次
  subscribeOnce(eventName: string, callback: Function): ISubscribe {
    // 初始化这个事件
    if (!this._eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this._eventObject[eventName] = {};
    }

    // 标示为只订阅一次的回调函数
    const id = "d" + this._callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this._eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this._eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this._eventObject[eventName]).length === 0) {
        delete this._eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 清除事件
  clear(eventName: string): void {
    // 未提供事件名称,默认清除所有事件
    if (!eventName) {
      this._eventObject = {};
      return;
    }

    // 清除指定事件
    delete this._eventObject[eventName];
  }
}

// 测试
interface IObj {
  msg: string;
}

type PublishType = [IObj, number];

const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj: IObj, num: number, s: string) => {
  console.log("模块A", obj, num);
});
eventBus.subscribe("eventX", (obj: IObj, num: number) => {
  console.log("模块B", obj, num);
});
eventBus.subscribe("eventX", (obj: IObj, num: number) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);

// 清除
eventBus.clear("eventX");

// 再次发布事件eventX,由于已经清除,所有模块都不会再收到消息了
eventBus.publish<PublishType>("eventX", { msg: "EventX published again!" }, 2);

// 输出
[LOG]: "模块A",  {
  "msg": "EventX published!"
},  1
[LOG]: "模块B",  {
  "msg": "EventX published!"
},  1
[LOG]: "模块C",  {
  "msg": "EventX published!"
},  1
[WRN]: "eventX not found!"

6. 单例模式

在实际使用过程中,往往只需要一个事件总线就能满足需求,这里有两种情况,保持在上层实例中单例和全局单例。

  1. 保持在上层实例中单例

将事件总线引入到上层实例使用,只需要保证在一个上层实例中只有一个 EventBus,如果上层实例有多个,意味着有多个事件总线,但是每个上层实例管控自己的事件总线。
首先在上层实例中建立一个变量用来存储事件总线,只在第一次使用时初始化,后续其他模块使用事件总线时直接取得这个事件总线实例。

代码

// 上层实例
class LWebApp {
  private _eventBus?: EventBus;

  constructor() {}

  public getEventBus() {
    // 第一次初始化
    if (this._eventBus == undefined) {
      this._eventBus = new EventBus();
    }

    // 后续每次直接取唯一一个实例,保持在LWebApp实例中单例
    return this._eventBus;
  }
}

// 使用
const eventBus = new LWebApp().getEventBus();
  1. 全局单例

有时候我们希望不管哪一个模块想使用我们的事件总线,我们都想这些模块使用的是同一个实例,这就是全局单例,这种设计能更容易统一管理事件。

写法同上面的类似,区别是要把 _eventBusgetEventBus 转为静态属性。使用时无需实例化 EventBusTool 工具类,直接使用静态方法就行了。

代码

// 上层实例
class EventBusTool {
  private static _eventBus?: EventBus;

  constructor() {}

  public static getEventBus(): EventBus {
    // 第一次初始化
    if (this._eventBus == undefined) {
      this._eventBus = new EventBus();
    }

    // 后续每次直接取唯一一个实例,保持全局单例
    return this._eventBus;
  }
}

// 使用
const eventBus = EventBusTool.getEventBus();

原文:https://dushusir.com/js-event-bus/

总结

以上是小编对 Event Bus 的一些理解,基本上实现了想要的效果。通过自己动手实现一遍发布订阅模式,也加深了对经典设计模式的理解。其中还有很多不足和需要优化的地方,欢迎大家多多分享自己的经验。

参考

LeetCode笔记:传递信息

原文https://lwebapp.com/zh/post/leetcode-send-message

问题

小朋友 A 在和 他的小伙伴们玩传信息游戏,游戏规则如下:

  1. n 名玩家,所有玩家编号分别为 0 ~ n-1,其中小朋友 A 的编号为 0
  2. 每个玩家都有固定的若干个可传信息的其他玩家(也可能没有)。传信息的关系是单向的(比如 A 可以向 B 传信息,但 B 不能向 A 传信息)。
  3. 每轮信息必须需要传递给另一个人,且信息可重复经过同一个人

给定总玩家数 n,以及按 [玩家编号,对应可传递玩家编号] 关系组成的二维数组 relation。返回信息从小 A (编号 0 ) 经过 k 轮传递到编号为 n-1 的小伙伴处的方案数;若不能到达,返回 0

示例 1:

输入:n = 5, relation = [[0,2],[2,1],[3,4],[2,3],[1,4],[2,0],[0,4]], k = 3

输出:3

解释:信息从小 A 编号 0 处开始,经 3 轮传递,到达编号 4。共有 3 种方案,分别是 0->2->0->4, 0->2->1->4, 0->2->3->4。

示例 2:

输入:n = 3, relation = [[0,2],[2,1]], k = 2

输出:0

解释:信息不能从小 A 处经过 2 轮传递到编号 2

限制:

  • 2 <= n <= 10
  • 1 <= k <= 5
  • 1 <= relation.length <= 90, 且 relation[i].length == 2
  • 0 <= relation[i][0],relation[i][1] < n 且 relation[i][0] != relation[i][1]

解法一

思路:

深度优先遍历(DFS),从位置 0 开始递归查找下一个位置,每次递归查到指定步数停止,停止时候判断目标位置是否满足要求,如果满足要求就计数加 1

代码:

/**
 * DFS
 * @param {number} n
 * @param {number[][]} relation
 * @param {number} k
 * @return {number}
 */
var numWays = function (n, relation, k) {
  // 统计路径数
  let ways = 0;
  const list = new Array(n).fill(0).map(() => new Array());

  // 将一个开始位置对应的多个传递位置搜集在一起,便于一起遍历传递位置
  for (const [from, to] of relation) {
    list[from].push(to);
  }

  const dfs = (index, step) => {
    // 当步数达到指定k步时传递到了n-1位置即满足要求
    if (step === k) {
      if (index === n - 1) {
        ways++;
      }
      // 无论有没有满足要求,走了k步就可以停止了
      return;
    }
    // 递归遍历list的所有路径
    const targetList = list[index];
    for (const nextIndex of targetList) {
      dfs(nextIndex, step + 1);
    }
  };

  // 第一步固定从1开始
  dfs(0, 0);
  return ways;
};

解法二

思路:

广度优先遍历(BFS),构造一个一维数组,将遍历到第 k 步所有的结果存储到这个数组中,最后再统计多少结果是满足要求的。

代码:

/**
   BFS
 * @param {number} n
 * @param {number[][]} relation
 * @param {number} k
 * @return {number}
 */
var numWays = function (n, relation, k) {
  const list = new Array(n).fill(0).map(() => new Array());

  // 将一个开始位置对应的多个传递位置搜集在一起,便于一起遍历传递位置
  for (const [from, to] of relation) {
    list[from].push(to);
  }

  // 计步器
  let step = 0;
  // 从起始位置0开始
  let queue = [0];
  // 1. 没有下一步目标不需要遍历
  // 2. 步数到了k就不需要遍历
  while (queue.length && step < k) {
    step++;
    // 取得当前queue的每一个位置,所对应的所有下一个位置,也存储进queue,同时把当前的每一个位置删除,因为已经走过了,这里是广度优先遍历和深度优先遍历的区别之处
    const length = queue.length;
    for (let i = 0; i < length; i++) {
      let index = queue.shift();
      let targetList = list[index];
      for (const nextIndex of targetList) {
        queue.push(nextIndex);
      }
    }
  }

  // 统计路径数
  let ways = 0;
  if (step === k) {
    while (queue.length) {
      if (queue.shift() === n - 1) {
        ways++;
      }
    }
  }
  return ways;
};

解法三

思路:

动态规划(DP),构造一个(k + 1) * n二维数组,将遍历到第 k 步所有的结果的计数存储到这个数组中,最后查看 k 步时 n - 1 的位置的计数就是方案数。

比如

var n = 5,
  relation = [
    [0, 2],
    [2, 1],
    [3, 4],
    [2, 3],
    [1, 4],
    [2, 0],
    [0, 4],
  ],
  k = 3;

构造一个 4 * 5 的数组,从第0步开始,arr[0][0]计为 1

0: (5) [1, 0, 0, 0, 0]
1: (5) [0, 0, 0, 0, 0]
2: (5) [0, 0, 0, 0, 0]
3: (5) [0, 0, 0, 0, 0]

第一轮

0: (5) [1, 0, 0, 0, 0]
1: (5) [0, 0, 1, 0, 1]
2: (5) [0, 0, 0, 0, 0]
3: (5) [0, 0, 0, 0, 0]

第二轮

0: (5) [1, 0, 0, 0, 0]
1: (5) [0, 0, 1, 0, 1]
2: (5) [1, 1, 0, 1, 0]
3: (5) [0, 0, 0, 0, 0]

第三轮

0: (5) [1, 0, 0, 0, 0]
1: (5) [0, 0, 1, 0, 1]
2: (5) [1, 1, 0, 1, 0]
3: (5) [0, 0, 1, 0, 3]

最后得到 第三轮结束时候,达到n - 1的方案数为 3

代码:

/**
 * @param {number} n
 * @param {number[][]} relation
 * @param {number} k
 * @return {number}
 */
var numWays = function (n, relation, k) {
  const dp = new Array(k + 1).fill(0).map(() => new Array(n).fill(0));
  dp[0][0] = 1;
  for (let i = 0; i < k; i++) {
    for (const [src, dst] of relation) {
      dp[i + 1][dst] += dp[i][src];
    }
  }
  return dp[k][n - 1];
};

更多leetcode算法题解,leetcode 刷题笔记,经典算法讲解 https://lwebapp.com/zh/tag/leetcode

参考

李佳琪直播间抢到了娇兰金钻粉底液!

背景

小编的媳妇常年关注李佳琪直播间,所以经常会剁手一些护肤品和化妆品,特别是现在比较火爆流行的化妆品,而且越是火爆的东西越难抢。

前几天就抢到了娇兰金钻粉底液,一个大瓶30ml和6个小样每个5ml。快递到手后立马试用了下,说是感觉效果很不错。

媳妇说,她平时化妆品用的不多,这个化妆品一次买的量又比较大,可以用很久。所以想是不是可以把小样出给集美们,让不想一次买那么多的集美们也能方便的试用一下,试用好的话就可以继续去官网买大瓶的,试用感觉不好的话,反正买的不多也不亏。

福利

我们看下官网的价格,750一个30ml的大瓶,价格已经劝退了没有用过的集美了

这个价格折算一个小样5ml的125一个,可见这个粉底液真的很受欢迎。媳妇说这个价值125的小样,打折给关注咱们公众号的朋友100包邮的价格出。

有朋友注意到,小红书和闲鱼上也有很多更便宜的卖这些护肤品的商家,大家一定要注意甄别,不是说价格便宜的一定不好,而是你想想为啥官网125价值的粉底液,为啥卖你半价不到,而且还能卖那么多?关键脸蛋是自己的,现在网上的假货实在太多了,大家一定要注意。

有意向的小伙伴,关注公众号:技术分社,后台回复:小编,即可添加微信咨询。

微信openHacking

总结

因为这个粉底液实在太抢手了,估计能很快出完。出完了也没有关系,之后抢到了其他好用的护肤品或者化妆品,我们还会考虑出一些给需要的小伙伴。

你可以先关注微信公众号或者直接添加文章底部的微信,有新的东西,我们再第一时间更新公众号和朋友圈。
谢谢大家的支持。

js toFixed精度问题兼容方法

原生的js的toFixed有精确度问题,需要兼容.

问题:

1.35.toFixed(1) // 1.4 正确
1.335.toFixed(2) // 1.33 错误
1.3335.toFixed(3) // 1.333 错误
1.33335.toFixed(4) // 1.3334 正确
1.333335.toFixed(5) // 1.33333 错误
1.3333335.toFixed(6) // 1.333333 错误

解决方法:

// toFixed兼容方法
Number.prototype.toFixed = function (n) {
    if (n > 20 || n < 0) {
        throw new RangeError('toFixed() digits argument must be between 0 and 20');
    }
    const number = this;
    if (isNaN(number) || number >= Math.pow(10, 21)) {
        return number.toString();
    }
    if (typeof (n) == 'undefined' || n == 0) {
        return (Math.round(number)).toString();
    }

    let result = number.toString();
    const arr = result.split('.');

    // 整数的情况
    if (arr.length < 2) {
        result += '.';
        for (let i = 0; i < n; i += 1) {
            result += '0';
        }
        return result;
    }

    const integer = arr[0];
    const decimal = arr[1];
    if (decimal.length == n) {
        return result;
    }
    if (decimal.length < n) {
        for (let i = 0; i < n - decimal.length; i += 1) {
            result += '0';
        }
        return result;
    }
    result = integer + '.' + decimal.substr(0, n);
    const last = decimal.substr(n, 1);

    // 四舍五入,转换为整数再处理,避免浮点数精度的损失
    if (parseInt(last, 10) >= 5) {
        const x = Math.pow(10, n);
        result = (Math.round((parseFloat(result) * x)) + 1) / x;
        result = result.toFixed(n);
    }

    return result;
};

参考自:https://www.jianshu.com/p/849b0ae36b36

CentOS 创建新用户并赋予 root 权限,禁止 root 登陆

原文:https://lwebapp.com/zh/post/centos-adduser

需求

最近遇到了阿里云 ECS 服务器被 DDoS 攻击的问题,说明我们的服务器安全性有待提高。从 ssh 登陆方面考虑,可以给 linux 系统设置子用户,禁止 root 登陆,能够提升一定的安全性。

本文你将会学习到

  • 如何给 linux 系统、CentOS 系统创建新用户,并设置密码
  • 如何给 CentOS 新用户授予权限控制
  • 如何禁止 root 登陆提升服务器 ssh 远程连接安全
  • 如何重启 sshd 服务
  • 如何查看文件权限及修改文件权限

用户管理

  1. 创建用户,设置密码

    先创建一个用户,起个名字,比如 lwebapp

    adduser lwebapp
    

    为用户 lwebapp 设置密码,会触发交互,输入密码即可

    passwd lwebapp # 触发交互,输入密码 lwebappwd
    
  2. 授予 root 权限
    修改sudoers,为新创建的用户赋予 root 权限,这样每次只需要用新用户lwebapp登录,就能完成所有服务器操作了。

    sudoers文件在/etc目录下,首先修改下这个文件的权限为允许修改

    chmod 777 /etc/sudoers
    

    然后用vim打开

    敲击i进入编辑模式

    vim /etc/sudoers
    

    接着在root ALL=(ALL) ALL这句话后面换行,输入

    lwebapp ALL=(ALL) ALL # 为lwebapp赋予所有权限,和上面的root一样
    

    敲击Esc,输入冒号:进入 vim 命令模式,再输入wq,敲击Enter保存退出

    保存完文件再把文件权限设置回来

    chmod 444 /etc/sudoers
    
  3. 禁止 root 登入
    因为有了新用户,就把 root 用户登陆的权限给禁止掉,这样黑客就没办法通过破解 root 登陆才操作服务器,至少我们的新用户名变了,给黑客攻击增加了一层难度

    找到并编辑sshd_config文件

    vim /etc/ssh/sshd_config
    

    找到PermitRootLogin yes,把yes改成no,意思就是不允许 root 账户登陆

    PermitRootLogin no
    
  4. 重启 sshd
    最后重启 sshd 才能生效

    systemctl restart sshd.service
    

原文:https://lwebapp.com/zh/post/centos-adduser

扩展学习

文件权限

  1. 查看文件权限
    stat -c '%A %a %n' *
    
  2. 让某个文件夹内所有文件都有 777 权限
    chmod 777 -R ./webapps
    

vim 基本操作

  1. 打开一个文件
    vim file.txt
    
  2. 进入编辑模式
    敲击 i,终端界面底部显示-- INSERT --即为编辑模式

  3. 进入命令模式
    输入:,终端界面底部显示:和光标

  4. 退出编辑模式或者命令模式
    敲击Esc

  5. 保存退出
    命令模式下,输入wq,敲击确定Enter即完成保存退出

  6. 强制退出
    命令模式下,输入q!,敲击确定Enter即完成强制退出

sshd 服务

  1. 查看 sshd 服务状态

    systemctl status sshd.service
    

    会显示一系列服务状态,比如running就表示是启动成功的状态

  2. 启动 sshd 服务

    systemctl start sshd.service
    
  3. 重启 sshd 服务
    systemctl restart sshd.service
    
  4. 设置为开机启动
    systemctl enable sshd.service
    

参考

Vmware Workstation16 出现 “VMware Workstation 不可恢复错误:(vcpu-1)”故障解决

原文:https://lwebapp.com/zh/post/vmware-vcpu-error

问题

小编最近准备在 Windows 电脑上安装 VMware Workstation 虚拟机,然后安装 macOS 系统用来做苹果系统环境的测试。

在安装 VMware 后,启动 macOS 虚拟机的时候,碰到了vcpu-0vcpu-1 报错,具体的报错关键信息有

VMware Workstation不可恢复错误:(vcpu-1)

Exception 0xc0000005 (access violation) has occurred.

还有

VMware Workstation 不可恢复错误: (vcpu-0)

在网上搜索了一些解决方案,试了好几种方案之后,又遇到了新的问题,macOS 虚拟机在启动的时候,会一直无限循环重启(注意,不是我的 Windows 宿主机系统重启,宿主机 Windows 未显示任何异常,VMware 也没有报错)。

上面的两个问题,小编尝试了以下列举的几个方法,成功解决问题。但是不知道具体是哪一个配置起了作用,遇到同样问题的朋友可以依次尝试。

解决

解决方案 1

进入 BIOS 将虚拟化技术 Intel Virtual Technology 设置,由Disable禁用设置为Enable可用.

具体设置步骤:

  1. 重启电脑或者重新开机,在成功开机之前,不停地点按F1键,即可进入 BIOS 界面(不同牌子的电脑不一样,F1 不行的话,再重启试试 F2 或者 F5、F8、Delete 都有可能,具体信息可以参考下自己品牌电脑官网说明)
  2. 进入 BIOS 后找到菜单 Advanced,进入 cpu configuration
  3. 将 intel virtual technology 设置为 Enable,然后保存退出;
  4. 成功开机后,重新打开虚拟机即可

一些品牌电脑 BIOS 系统中的开启虚拟化技术的位置

  • 某些 HP(惠普)电脑进入 BIOS 后
    1. 需要选择 System Configuration(系统配置)菜单
    2. 然后选择 Device Configuration(设备配置)
    3. 找到 Virtualization Technology,设置为 Enabled
  • 某些联想 Thinkpad 电脑进入 BIOS 后
    1. 需要选择 Security 菜单
    2. 然后选择 Virtualization,设置为 Enabled
  • 某些 DELL(戴尔)电脑进入 BIOS 后
    1. 需要选择 Processor Settings 菜单
    2. 然后选择 Virtualization Technology,设置为 Enabled

解决方案 2

修复 VMWare Workstation

具体设置步骤:

  1. 打开“控制面板”,进入“卸载程序”

  2. 双击 VMWare,不要选择“卸载”,选择“修复”

  3. 完成后,再打开虚拟机即恢复正常

解决方案 3

修改配置文件 macOS.vmx

具体设置步骤:

  1. 在设置的虚拟机挂载的磁盘目录找到 macOS.vmx 文件,用 NotePad 记事本打开这个文件
  2. 确保文件默认添加了这两行代码
smc.version = 0
cpuid.1.eax = "00000000000000010000011010100101"

解决方案 4

禁用 Hyper-V, 然后重启电脑即可

具体设置步骤:

PowerShell 或者 CMD.exe 管理员模式运行:

 # 关闭 Hyper-v,支持VMWare Workstation运行
bcdedit /set hypervisorlaunchtype off

提醒,小编之前在 Windows 还下载安装过 Docker Windows Desktop 软件,用来在 Windows 电脑本地运行 Docker 服务做测试用,发现 Docker 需要开启 Hyper-v 才能用,所以这里是有冲突的,暂时的方案就是用 VMware 虚拟机的时候关闭 Hyper-v,用 Docker 的时候再打开 Hyper-v。用以下命令来重新开启 Hyper-v。

PowerShell 或者 CMD.exe 管理员模式运行:

# 开启 Hyper-v,支持Docker运行,VMWare Workstation无法运行
bcdedit /set hypervisorlaunchtype auto

原文:https://lwebapp.com/zh/post/vmware-vcpu-error

总结

如果以上方法都不能解决,推荐咨询官方专家。如果您发现了其他更有效直接的解决方案,也欢迎告诉小编。

参考

webrtc 屏幕录制

需求

我们在复现软件问题,或者做教学的时候,需要录屏,一般来说需要下载屏幕录制软件,经常还会带上水印,有没有一种可以不用安装软件在线录屏的方案呢,答案就是webrtc,使用浏览器自带的功能,打开网页就能录制桌面上的任意窗口,而且没有水印。

演示

https://dushusir.com/tool/web-recorder.html

代码

以下代码你将学习到
– 使用navigator.mediaDevices.getDisplayMedia获取用户窗口
– 使用MediaRecorder获取用户窗口录制的视频流
– 使用Blob和window.URL.createObjectURL来将视频流下载下来

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>webrtc screen recorder</title>
</head>

<body>


    <video autoplay playsinline id="player"></video>
    <video playsinline id="recordPlayer"></video>

    <!-- 在开始录制之前播放和下载按钮时禁用的 -->
    <button id="recordBtn">start</button>
    <button id="playBtn">play</button>
    <button id="downloadBtn">download</button>

    <script type="text/javascript">


        var player = document.querySelector("#player");
        var recordPlayer = document.querySelector("#recordPlayer");
        var recordBtn = document.querySelector("#recordBtn");
        var playBtn = document.querySelector("#playBtn");
        var downloadBtn = document.querySelector("#downloadBtn");

        var buffer; // 用于存储录制数据(数组)
        var mediaStream;
        var mediaRecoder;

        // 录制按钮点击事件
        recordBtn.addEventListener('click', function () {
            // console.log(recordBtn.text());
            if (recordBtn.textContent === 'start') {
                startRecord();
                recordBtn.textContent = 'stop';
                playBtn.setAttribute('disabled', true);
                downloadBtn.setAttribute('disabled', true);
            } else if (recordBtn.textContent === 'stop') {
                stopRecord();
                recordBtn.textContent = 'start';
                playBtn.removeAttribute('disabled');
                downloadBtn.removeAttribute('disabled');
            }
        });

        // 播放按钮点击事件
        playBtn.addEventListener('click', function () {
            var blob = new Blob(buffer, { type: 'video/webm' });
            // 根据缓存数据生成url给recordPlayer进行播放
            recordPlayer.src = window.URL.createObjectURL(blob);
            recordPlayer.srcObject = null;
            recordPlayer.controls = true; // 显示播放控件
        });

        // 下载按钮点击事件
        downloadBtn.addEventListener('click', function () {
            var blob = new Blob(buffer, { type: 'video/webm' });
            // 根据缓存数据生成url
            var url = window.URL.createObjectURL(blob);
            // 创建一个a标签,通过a标签指向url来下载
            var a = document.createElement('a');
            a.href = url;
            a.style.display = 'none'; // 不显示a标签
            a.download = 'test.webm'; // 下载的文件名
            a.click(); // 调用a标签的点击事件进行下载
        });

        // 开始录制
        function start() {

            var options = { mimeType: 'video/webm;codecs=vp8' };
            if (!MediaRecorder.isTypeSupported(options.mimeType)) {
                console.log('不支持' + options.mimeType);
                return;
            }

            try {
                buffer = [];
                mediaRecoder = new MediaRecorder(mediaStream, options);
            } catch (e) {
                console.log('创建MediaRecorder失败!');
                return;
            }
            mediaRecoder.ondataavailable = handleDataAvailable;
            // 开始录制,设置录制时间片为10ms(每10s触发一次ondataavilable事件)
            mediaRecoder.start(1000);
        }

        // 停止录制
        function stopRecord() {
            mediaRecoder.stop();
            mediaStream.getTracks().forEach(track => track.stop())
        }

        // 触发ondataavilable事件的回调函数
        function handleDataAvailable(e) {
            if (e && e.data && e.data.size > 0) {
                buffer.push(e.data);
            }
        }

        function startRecord() {
            if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
                console.log('不支采集音视频数据!');
            } else {
                // 采集音频数据
                var constrants = {
                    video: true,
                    audio: true
                };
                navigator.mediaDevices.getDisplayMedia(constrants).then(gotMediaStream).catch(handleError);

            }
        }

        // 采集音频数据成功时调用的方法
        function gotMediaStream(stream) {
            mediaStream = stream;
            player.srcObject = stream;

            start()
        }

        // 采集音频数据失败时调用的方法
        function handleError(err) {
            console.log(err.name + ':' + err.message);
        }
    </script>
</body>

</html>

上海疫苗第三针加强针补贴最新信息|持续更新0213

背景

自年前1.26日以来至今日,上海地区已经连续无新增本地新冠病例了,可以说控制的非常棒。

这需要我们继续坚持防疫政策,听从指挥,总有一天疫情会结束的。

现在正在积极开放新冠疫苗第三剂加强针的接种,可以看到很多地铁口都有志愿者或者医务人员举着牌子告诉大家可以接种新冠疫苗了。身边的很多朋友也积极配合去接种了。

小编从业内人士了解到,新冠疫苗第二针接种超过6个月,就可以继续接种第三针加强针了,部分地区还开放了补助名额。小编给大家争取到了一些补贴福利,可以看看以下的补贴要求,看看自己是否可以接种了,一旦时间到了就可以接种了,暂时没有到时间的,也可以先预约着,到了时间直接过去。

最新补贴消息

上海加强针现场补贴🧧130元

要求

  1. 全国打的二针都可以(本社区也要),风险地区不要
  2. 18周岁~60周岁,距离北京生物和北京科兴第二针和康希诺,必须接种半年以上,也就是2021年8月12号之前接种第二针疫苗的
  3. 不要深圳康泰,智飞
  4. 健康码是绿码

注意事项

到现场必须打电话,带身份证原件智能手机,凭小票结账

集合时间

9点30开始

地址

杨浦区

详细了解更多信息 请添加联系人微信

关注文末公众号(技术分社),回复:疫苗,即可获取联系人信息

总结

月底名额开放更多,欢迎添加联系人信息免费咨询。到了时间还没接种第三针疫苗的,要抓紧时间了。

提醒事项

去打第三针加强针疫苗的同事已经领到补贴了。要真正领到补贴,要注意小编接下来要提醒的注意事项。

  1. 关于新冠疫苗接种第三针加强针相关补贴消息,有新消息都会及时更新,因为现在补贴指标很快就满,名额有限,这些地区指标满了之后,就没有补贴了,暂时没有名额的也不用着急,关注后续的补贴信息

  2. 之前发布的补贴地址可能会经常更新。去之前请一定联系好补贴负责人,联系下确认那个地方还有补贴再去,免得白跑一趟。

报名

关注公众号: 技术分社

微信openHacking

  • 回复:疫苗或者补贴. 即可获取疫苗补贴负责人的微信

如果您已经拿到了补贴,也欢迎回来给小编加个鸡腿🍗,让他给大家整理更多补贴优惠信息。

实现人手一个冰墩墩python画冰墩墩源码

背景

最近北京冬奥会正在举行,北京冬奥会的吉祥物“冰墩墩”非常抢手,很多朋友连夜排队都抢不到一个冰墩墩。

冰墩墩实在太火了,为了实现冰墩墩自由、“人手一墩”的梦想,强大的程序员GG奉上了一个用Python绘制冰墩墩的程序,先看下面这个动图体会下

真是太强大了,网友真是无所不能

思路和代码

技术上主要用到了python的turtle和tkinter模块,这两个模块都是Python内置的模块,非常强大。

  • turtle用来绘图,有多种绘制方式和样式
  • tkinter用来构建GUI程序,也内置了许多组件

主程序基本上就是采用turtle,设置不同的画笔和路径等绘制方式,一笔一笔接着在初始化好的tkinter Canvas画布上画出来的。核心功能也就完成了。

先启动一个tkinter界面,放置一个Canvas画布,再将turtle绘制区域设置到这个Canvas上

root = Tk()
root.geometry('600x700+500+60')
root.config(bg='white')
root.title('lwebapp.com')
root.resizable(False, False)
canvas = Canvas(root, width=600, height=600)
canvas.place(x=0,y=50)

t = RawTurtle(canvas)
t.hideturtle()

完整的代码到源码的main.py(https://github.com/openHacking/TKinter-UI/blob/main/demo/draw_bdd/main.py)文件查看

接着就是核心的绘制代码,由于代码太长这里贴出一部分

def draw_bdd(t):

    # reset
    t.penup()
    t.home()
    t.clear()

    # adjust the speed
    t.speed(30)

    # left hand
    t.goto(177, 112)

    t.pencolor("lightgray")

    t.pensize(3)

    t.fillcolor("white")

    t.begin_fill()

    t.pendown()

    t.setheading(80)

    t.circle(-45, 200)

    t.circle(-300, 23)

    t.end_fill()

完整的代码到源码的draw.py(https://github.com/openHacking/TKinter-UI/blob/main/demo/draw_bdd/draw.py)文件查看

最后,用 pytxui 这个python GUI组件库放了两个样式更美观的按钮做重新绘制和下载功能。

下载功能用到了Pillow的ImageGrab截图功能,可以将Canvas上的冰墩墩绘制结果保存为一张PNG图片。

核心的截图保存代码

from PIL import ImageGrab
import os

def save_image(root,widget):
    root.update()
    x=root.winfo_rootx()+widget.winfo_x()
    y=root.winfo_rooty()+widget.winfo_y()

    x1=x+widget.winfo_width()
    y1=y+widget.winfo_height()
    offset = 4
    ImageGrab.grab().crop((x + offset,y + offset,x1 - offset,y1 - offset)).save(os.getcwd() + "\\bdd.png", "PNG")

源码

pytxui-draw_bdd (https://github.com/openHacking/TKinter-UI/tree/main/demo/draw_bdd)

总结

以上我们结合当前大火的冰墩墩学习到了Python相关的知识

  • turtle和tkinter结合使用构造GUI
  • 如何构造一个漂亮的GUI按钮组件
  • 如何将tkinter Canvas画布下载为图片

其中还有很多不足,欢迎给作者多提建议和想法,一起学习提高。

参考

  • pytxui (https://github.com/openHacking/TKinter-UI)