恋上数据结构与算法之二叉堆

文章目录

    • 需求分析
    • Top K 问题
    • 堆的基本接口设计
    • 二叉堆(Binary Heap)
    • 最大堆
      • 添加
        • 思路
        • 交换位置的优化
        • 实现
      • 删除
        • 思路
        • 流程图解
        • 实现
      • replace
      • 批量建堆
        • 自上而下的上滤
        • 自下而上的下滤
        • 效率对比
        • 复杂度计算
        • 实现
      • 完整代码
    • 最小堆
    • 比较器解析
    • Top K 问题
      • 问题分析
      • 代码实现
      • 内部方法分析
      • 问题 2
    • 堆排序
      • 概念
      • 代码示例
        • 第一种 -- 降序
        • 第二种 -- 升序
      • 空间复杂度能否下降至 O(1)?
        • 示例代码分析
        • 示例代码分析

需求分析

在这里插入图片描述

Top K 问题

什么是 Top K 问题?

从海量数据中找出前 K 个数据。

  • 比如:从 100 万个整数中找出最大的 100 个整数
  • Top K 问题的解法之一:可以用数据结构 “” 来解决。

堆是一种【完全二叉树】,可以分为【最大堆】和【最小堆】。只要是堆,里面的元素就会具备可比较性。

  • 在最大堆中,父节点的值大于等于(>=)其子节点的值;
  • 在最小堆中,父节点的值小于等于(<=)其子节点的值。

在这里插入图片描述

堆的基本接口设计

public interface Heap<E> {
	int size();	// 元素的数量
	boolean isEmpty();	// 是否为空
	void clear();	// 清空
	void add(E element);	 // 添加元素
	E get();	// 获得堆顶元素
	E remove(); // 删除堆顶元素
	E replace(E element); // 删除堆顶元素的同时插入一个新元素
}

二叉堆(Binary Heap)

着重注意索引的规律

在这里插入图片描述

floor(向下取整):只取前面的整数。

最大堆

添加

思路

一步步往上与父节点比较,并进行位置交换。

在这里插入图片描述

交换位置的优化

一般交换位置需要 3 行代码,可以进一步优化

  • 将新添加节点备份,确定最终位置才摆放上去
  • 循环比较,交换父节点位置 -> 循环比较,单纯父节点下移,最后确定位置了直接覆盖
  • 省去了每次都交换位置并且覆盖的操作
实现
	@Override
	public void add(E element) {
		elementNotNullCheck(element);
		ensureCapacity(size + 1);
		elements[size++] = element;
		siftUp(size - 1);
	}

	private void elementNotNullCheck(E element) {
		if (element == null) {
			throw new IllegalArgumentException("element must not be null");
		}
	}

	private void ensureCapacity(int capacity) {
		int oldCapacity = elements.length;
		if (oldCapacity >= capacity) {
			return;
		}
		
		// 新容量为旧容量的1.5倍
		int newCapacity = oldCapacity + (oldCapacity >> 1);
		E[] newElements = (E[]) new Object[newCapacity];
		for (int i = 0; i < size; i++) {
			newElements[i] = elements[i];
		}
		elements = newElements;
	}

	/**
	 * 让index位置的元素上滤
	 * @param index
	 */
	private void siftUp(int index) {
//		E e = elements[index];
//		while (index > 0) {
//			int pindex = (index - 1) >> 1;
//			E p = elements[pindex];
//			if (compare(e, p) <= 0) return;
//			
//			// 交换index、pindex位置的内容
//			E tmp = elements[index];
//			elements[index] = elements[pindex];
//			elements[pindex] = tmp;
//			
//			// 重新赋值index
//			index = pindex;
//		}
		E element = elements[index];
		while (index > 0) {
			int parentIndex = (index - 1) >> 1;
			E parent = elements[parentIndex];
			if (compare(element, parent) <= 0) {
				break;
			}
			
			// 将父元素存储在index位置
			elements[index] = parent;
			
			// 重新赋值index
			index = parentIndex;
		}
		elements[index] = element;
	}

删除

思路

一般来说,如果我们要删除某个元素的话,我们通常会拿到最后一个元素先覆盖它的位置,然后再把最后一个元素删掉,相当于同学们直接将 43 的值覆盖掉 0 这个位置的值,要再把这个值清空。

为什么?

因为这个操作是 O(1) 级别的,删除最后一个元素。

具体流程如下图所示:
在这里插入图片描述

流程图解

在这里插入图片描述

实现
	@Override
	public E remove() {
		emptyCheck();
		
		int lastIndex = --size;
		E root = elements[0];
		elements[0] = elements[lastIndex];
		elements[lastIndex] = null;
		
		siftDown(0);
		return root;
	}

	/**
	 * 让index位置的元素下滤
	 * @param index
	 */
	private void siftDown(int index) {
		E element = elements[index];
		int half = size >> 1;
		// 第一个叶子节点的索引 == 非叶子节点的数量
		// index < 第一个叶子节点的索引
		// 必须保证index位置是非叶子节点
		while (index < half) { 
			// index的节点有2种情况
			// 1.只有左子节点
			// 2.同时有左右子节点
			
			// 默认为左子节点跟它进行比较
			int childIndex = (index << 1) + 1;
			E child = elements[childIndex];
			
			// 右子节点
			int rightIndex = childIndex + 1;
			
			// 选出左右子节点最大的那个
			if (rightIndex < size && compare(elements[rightIndex], child) > 0) {
				child = elements[childIndex = rightIndex];
			}
			
			if (compare(element, child) >= 0) {
				break;
			}

			// 将子节点存放到index位置
			elements[index] = child;
			// 重新设置index
			index = childIndex;
		}
		elements[index] = element;
	}

replace

接口:删除堆顶元素的同时插入一个新元素

	@Override
	public E replace(E element) {
		elementNotNullCheck(element);
		
		E root = null;
		if (size == 0) {
			elements[0] = element;
			size++;
		} else {
			root = elements[0];
			elements[0] = element;
			siftDown(0);
		}
		return root;
	}

批量建堆

批量建堆,有 2 种做法

  1. 自上而下的上滤 – 本质是添加
  2. 自下而上的下滤 – 本质是删除

注意:【自上而下的滤】和【自下而上的滤】不可以批量建堆,因为执行起来对整体来说没有什么贡献,依然还是乱的。

自上而下的上滤

在这里插入图片描述

自下而上的下滤

在这里插入图片描述

效率对比
  • 如下图所示,显然是【自下而上的下滤】效率更高。
  • 可把图中蓝色部分看作是节点数量,箭头直线看作是工作量。
  • 最下层的节点最多,这一部分在【自下而上的下滤】中的工作量较小。

在这里插入图片描述

复杂度计算

深度之和 vs 高度之和

在这里插入图片描述

公式推导

在这里插入图片描述

实现

1、修改构造函数

	public BinaryHeap(E[] elements, Comparator<E> comparator)  {
		super(comparator);
		
		if (elements == null || elements.length == 0) {
			this.elements = (E[]) new Object[DEFAULT_CAPACITY];
		} else {
			size = elements.length;
       // this.elements = elements // 不能这么写,因为不安全
			int capacity = Math.max(elements.length, DEFAULT_CAPACITY);
			this.elements = (E[]) new Object[capacity];
			for (int i = 0; i < elements.length; i++) {
				this.elements[i] = elements[i];
			}
      // 批量建堆
			heapify();
		}
	}

解释:

this.elements = elements 会导致外部传进来的数组和堆内的数组挂钩,如果后续修改了外包数组的元素值,会影响批量建堆的输出。

2、批量建堆方法编写

	/**
	 * 批量建堆
	 */
	private void heapify() {
		// 自上而下的上滤
//		for (int i = 1; i < size; i++) {
//			siftUp(i);
//		}
		
		// 自下而上的下滤
		for (int i = (size >> 1) - 1; i >= 0; i--) {
			siftDown(i);
		}
	}

完整代码

/**
 * 二叉堆(最大堆)
 */
@SuppressWarnings("unchecked")
public class BinaryHeap<E> extends AbstractHeap<E> implements BinaryTreeInfo {
	private E[] elements;
	private static final int DEFAULT_CAPACITY = 10;
	
	public BinaryHeap(E[] elements, Comparator<E> comparator)  {
		super(comparator);
		
		if (elements == null || elements.length == 0) {
			this.elements = (E[]) new Object[DEFAULT_CAPACITY];
		} else {
			size = elements.length;
			int capacity = Math.max(elements.length, DEFAULT_CAPACITY);
			this.elements = (E[]) new Object[capacity];
			for (int i = 0; i < elements.length; i++) {
				this.elements[i] = elements[i];
			}
			heapify();
		}
	}
	
	public BinaryHeap(E[] elements)  {
		this(elements, null);
	}
	
	public BinaryHeap(Comparator<E> comparator) {
		this(null, comparator);
	}
	
	public BinaryHeap() {
		this(null, null);
	}

	@Override
	public void clear() {
		for (int i = 0; i < size; i++) {
			elements[i] = null;
		}
		size = 0;
	}

	@Override
	public void add(E element) {
		elementNotNullCheck(element);
		ensureCapacity(size + 1);
		elements[size++] = element;
		siftUp(size - 1);
	}

	@Override
	public E get() {
		emptyCheck();
		return elements[0];
	}

	@Override
	public E remove() {
		emptyCheck();
		
		int lastIndex = --size;
		E root = elements[0];
		elements[0] = elements[lastIndex];
		elements[lastIndex] = null;
		
		siftDown(0);
		return root;
	}

	@Override
	public E replace(E element) {
		elementNotNullCheck(element);
		
		E root = null;
		if (size == 0) {
			elements[0] = element;
			size++;
		} else {
			root = elements[0];
			elements[0] = element;
			siftDown(0);
		}
		return root;
	}
	
	/**
	 * 批量建堆
	 */
	private void heapify() {
		// 自上而下的上滤
//		for (int i = 1; i < size; i++) {
//			siftUp(i);
//		}
		
		// 自下而上的下滤
		for (int i = (size >> 1) - 1; i >= 0; i--) {
			siftDown(i);
		}
	}
	
	/**
	 * 让index位置的元素下滤
	 * @param index
	 */
	private void siftDown(int index) {
		E element = elements[index];
		int half = size >> 1;
		// 第一个叶子节点的索引 == 非叶子节点的数量
		// index < 第一个叶子节点的索引
		// 必须保证index位置是非叶子节点
		while (index < half) { 
			// index的节点有2种情况
			// 1.只有左子节点
			// 2.同时有左右子节点
			
			// 默认为左子节点跟它进行比较
			int childIndex = (index << 1) + 1;
			E child = elements[childIndex];
			
			// 右子节点
			int rightIndex = childIndex + 1;
			
			// 选出左右子节点最大的那个
			if (rightIndex < size && compare(elements[rightIndex], child) > 0) {
				child = elements[childIndex = rightIndex];
			}
			
			if (compare(element, child) >= 0) {
				break;
			}

			// 将子节点存放到index位置
			elements[index] = child;
			// 重新设置index
			index = childIndex;
		}
		elements[index] = element;
	}
	
	/**
	 * 让index位置的元素上滤
	 * @param index
	 */
	private void siftUp(int index) {
//		E e = elements[index];
//		while (index > 0) {
//			int pindex = (index - 1) >> 1;
//			E p = elements[pindex];
//			if (compare(e, p) <= 0) return;
//			
//			// 交换index、pindex位置的内容
//			E tmp = elements[index];
//			elements[index] = elements[pindex];
//			elements[pindex] = tmp;
//			
//			// 重新赋值index
//			index = pindex;
//		}
		E element = elements[index];
		while (index > 0) {
			int parentIndex = (index - 1) >> 1;
			E parent = elements[parentIndex];
			if (compare(element, parent) <= 0) {
				break;
			}
			
			// 将父元素存储在index位置
			elements[index] = parent;
			
			// 重新赋值index
			index = parentIndex;
		}
		elements[index] = element;
	}
	
	private void ensureCapacity(int capacity) {
		int oldCapacity = elements.length;
		if (oldCapacity >= capacity) {
			return;
		}
		
		// 新容量为旧容量的1.5倍
		int newCapacity = oldCapacity + (oldCapacity >> 1);
		E[] newElements = (E[]) new Object[newCapacity];
		for (int i = 0; i < size; i++) {
			newElements[i] = elements[i];
		}
		elements = newElements;
	}
	
	private void emptyCheck() {
		if (size == 0) {
			throw new IndexOutOfBoundsException("Heap is empty");
		}
	}
	
	private void elementNotNullCheck(E element) {
		if (element == null) {
			throw new IllegalArgumentException("element must not be null");
		}
	}

	@Override
	public Object root() {
		return 0;
	}

	@Override
	public Object left(Object node) {
		int index = ((int)node << 1) + 1;
		return index >= size ? null : index;
	}

	@Override
	public Object right(Object node) {
		int index = ((int)node << 1) + 2;
		return index >= size ? null : index;
	}

	@Override
	public Object string(Object node) {
		return elements[(int)node];
	}
}

最小堆

同样使用最大堆的代码,只需要设置一个倒序比较器即可,将小的数认为比较大放在数组前面。

代码如下:

	static void test3() {
		Integer[] data = {88, 44, 53, 41, 16, 6, 70, 18, 85, 98, 81, 23, 36, 43, 37};
		BinaryHeap<Integer> heap = new BinaryHeap<>(data, new Comparator<Integer>() {
			@Override
			public int compare(Integer o1, Integer o2) {
        // 将原本【最大堆】中较小的值放前面,就实现了【最小堆】
				return o2 - o1;
			}
		});
		BinaryTrees.println(heap);
	}

	public static void main(String[] args) {
		test3();
	}

比较器解析

无论是 o1 - o2 还是 o2 - o1

  1. 只要返回正整数,就表示 o1 应该在 o2 的右边。
  2. 而返回负整数则表示 o1 应该在 o2 的左边。

示例说明:

1、向数组中加入 20,Integer[] data = {10, 20}

o1 - o2 = -10 – 10, 20

o2 - o1 = 10 – 20, 10

2、再向数组中加入 30,Integer[] data ={10, 20, 30}

o1 - o2

  • 10 - 30 = -20 – 10, 30, 20
  • 20 - 30 = -10 – 【10, 20, 30】

o2 - o1

  • 30 - 10 = 20 – 30, 10
  • 20 - 10 = 10 – 【30, 20, 10】

总结

无论是升序还是降序,只要返回正整数,就表示第一个元素应该在第二个元素的右边。

Top K 问题

问题分析

题目:从 n 个整数中,找出最大的前 k 个数 (k 远远小于 n)

  1. 如果使用【排序算法】进行全排序,需要 O(nlogn) 的时间复杂度。

  2. 如果使用【二叉堆】来解决,可以使用 O(nlogk) 的时间复杂度来解决

    • 新建一个小顶堆
    • 扫描 n 个整数

具体细节:

  1. 先将遍历到的前 k 个数放入堆中;
  2. 从第 k + 1 个数开始,如果大于堆顶元素,就使用 replace 操作(删除堆顶元素,将第 k + 1 个数添加到堆中)

代码实现

	static void test4() {
		// 新建一个小顶堆
		BinaryHeap<Integer> heap = new BinaryHeap<>(new Comparator<Integer>() {
			@Override
			public int compare(Integer o1, Integer o2) {
				return o2 - o1;
			}
		});
		
		// 找出最大的前k个数
		int k = 3;
		Integer[] data = {51, 30, 39, 92, 74, 25, 16, 93, 
				91, 19, 54, 47, 73, 62, 76, 63, 35, 18, 
				90, 6, 65, 49, 3, 26, 61, 21, 48};
		for (int i = 0; i < data.length; i++) {
			if (heap.size() < k) { // 前k个数添加到小顶堆
				heap.add(data[i]); // logk
			} else if (data[i] > heap.get()) { // 如果是第k + 1个数,并且大于堆顶元素
				heap.replace(data[i]); // logk
			}
		}
		// O(nlogk)
		BinaryTrees.println(heap);
	}

	public static void main(String[] args) {
		test4();
	}

内部方法分析

1、heap.get() 获取堆顶元素

	@Override
	public E get() {
		emptyCheck();
		return elements[0];
	}

2、heap.replace(data[i]);

删除堆顶元素的同时插入一个新元素,即将大于堆顶的数组元素加进去。

3、这是个最小堆

堆顶元素一直是最小的。

问题 2

如果是找出最小的前 k 个数呢?

  1. 顶堆
  2. 如果小于堆顶元素,就使用 replace 操作

堆排序

概念

堆排序(Heap Sort)是一种基于堆数据结构的排序算法,它利用了堆的特性来进行排序。

堆排序的基本思想如下:

  1. 构建最大堆(或最小堆):将待排序的数组构建成一个最大堆(或最小堆)。
  2. 交换堆顶元素:将堆顶元素与当前未排序部分的最后一个元素交换位置。
  3. 调整堆:对交换后的堆进行调整,使其满足最大堆(或最小堆)的性质。
  4. 重复步骤 2 和 3,直到整个数组排序完成。

代码示例

以下是两个简单的堆排序示例代码:

第一种 – 降序
public class Main2 {
    public static void main(String[] args) {
        Integer[] arr = { 12, 11, 13, 5, 6, 7 };
        BinaryHeap<Integer> heap = new BinaryHeap<>(arr);
        heapSort(heap);
    }

    public static <E> void heapSort(BinaryHeap<E> heap) {
        int size = heap.size();
        for (int i = 0; i < size; i++) {
            // 删除后会再调整堆结构
            E max = heap.remove();
            System.out.print(max + " ");
        }
    }
}

输出结果为:

13 12 11 7 6 5 
第二种 – 升序
public class HeapSort {
    public static void heapSort(Integer[] arr) {
        int n = arr.length;

        // 构建最大堆
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapify(arr, n, i);
        }

        // 交换堆顶元素和未排序部分的最后一个元素,并调整堆
        for (int i = n - 1; i > 0; i--) {
            // 将堆顶元素与当前未排序部分的最后一个元素交换位置
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;

            // 调整堆
            heapify(arr, i, 0);
        }
    }

    // 调整堆,使其满足最大堆的性质
    public static void heapify(Integer[] arr, int n, int i) {
        int largest = i; // 初始化堆顶元素为最大值
        int left = 2 * i + 1; // 左子节点的索引
        int right = 2 * i + 2; // 右子节点的索引

        // 判断左子节点是否大于堆顶元素
        if (left < n && arr[left] > arr[largest]) {
            largest = left;
        }

        // 判断右子节点是否大于堆顶元素
        if (right < n && arr[right] > arr[largest]) {
            largest = right;
        }

        // 如果堆顶元素不是最大值,则交换堆顶元素和最大值,并继续调整堆
        if (largest != i) {
            int temp = arr[i];
            arr[i] = arr[largest];
            arr[largest] = temp;

            heapify(arr, n, largest);
        }
    }

    public static void main(String[] args) {
        Integer[] arr = { 12, 11, 13, 5, 6, 7 };
        
        System.out.println("原始数组:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println("\n------------------------");

        BinaryHeap<Integer> heap = new BinaryHeap<>(arr);
        BinaryTrees.println(heap);

        System.out.println("==========================");

        heapSort(arr);

        System.out.println("\n排序后的数组:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println("\n------------------------");

        BinaryHeap<Integer> heap2 = new BinaryHeap<>(arr);
        BinaryTrees.println(heap2);

    }
}

输出结果为:

原始数组:
12 11 13 5 6 7 
------------------------
   ┌──13─┐
   │     │
┌─11─┐ ┌─12
│    │ │
5    6 7
==========================

排序后的数组:
5 6 7 11 12 13 
------------------------
    ┌──13─┐
    │     │
 ┌─12─┐ ┌─7
 │    │ │
11    6 5

注意:

以下这个方法是会对【原数组】的值改变的,heapSort 方法会直接修改原始数组。这意味着在排序之后,原始数组的顺序会被改变。

如果你希望保持原始数组的不变,并在排序后得到一个新的已排序副本,可以使用以下方法:


// 在进行堆排序之前,创建一个原始数组的副本。
Integer[] arr = { 12, 11, 13, 5, 6, 7 };
Integer[] arrCopy = Arrays.copyOf(arr, arr.length);

空间复杂度能否下降至 O(1)?

在当前的实现中,二叉堆的空间复杂度是 O(n),其中 n 是元素的数量。这是因为我们使用一个数组来存储堆的元素。

  • 要将空间复杂度降低到 O(1),我们需要修改数据结构的实现方式

  • 目前的实现方式(BinaryHeap<E>)是使用一个动态数组来存储元素,但这会占用 O(n) 的额外空间。

如果要将空间复杂度降低到 O(1),我们可以考虑使用原始的输入数组来表示堆,而不是创建一个额外的数组。这意味着我们需要在原始数组上进行堆操作,而不是将元素复制到新的数组中。(比如上面写的第二种代码示例)

但是,这样做会对原始数组进行修改,并且在堆操作期间可能会打乱原始数组的顺序。因此,在实际应用中,这种修改可能会有限制,并且需要权衡空间和时间的复杂度。

总结起来,要将空间复杂度降低到 O(1),需要在原始数组上进行操作,但这可能会对原始数组造成修改,并可能会有限制和权衡。具体的实现方式取决于具体的应用场景和需求。

示例代码分析

第一种示例方法复杂度解析

空间复杂度

如果输入的元素个数为 n,且 n 大于 10,那么空间复杂度为 O(n);否则,空间复杂度为 O(1)。

所以最坏空间复杂度为 O(n)

时间复杂度

  • 构建二叉堆的过程具有 O(n) 的时间复杂度,其中 n 是输入数组的长度。
  • 接下来,进行 n 次删除操作,每次删除操作的时间复杂度为 O(logn)。由于删除操作会调整堆的结构,保持最大堆的性质,因此每次删除操作的时间复杂度为O(logn)。
  • 因此,总体上,堆排序的时间复杂度为 O(nlogn)

第二种示例方法复杂度解析

空间复杂度

  • heapSort 方法中,除了输入数组之外,没有使用额外的空间。因此,空间复杂度为 O(1),即常数级别的空间复杂度。

时间复杂度:

  • 构建最大堆的过程具有 O(n) 的时间复杂度,其中 n 是输入数组的长度。

  • 接下来,进行 n-1 次堆调整和交换操作,每次操作的时间复杂度为 O(logn)。

  • 因此,总体上,堆排序的时间复杂度为 O(nlogn)
    *。

  • 目前的实现方式(BinaryHeap<E>)是使用一个动态数组来存储元素,但这会占用 O(n) 的额外空间。

如果要将空间复杂度降低到 O(1),我们可以考虑使用原始的输入数组来表示堆,而不是创建一个额外的数组。这意味着我们需要在原始数组上进行堆操作,而不是将元素复制到新的数组中。(比如上面写的第二种代码示例)

但是,这样做会对原始数组进行修改,并且在堆操作期间可能会打乱原始数组的顺序。因此,在实际应用中,这种修改可能会有限制,并且需要权衡空间和时间的复杂度。

总结起来,要将空间复杂度降低到 O(1),需要在原始数组上进行操作,但这可能会对原始数组造成修改,并可能会有限制和权衡。具体的实现方式取决于具体的应用场景和需求。

示例代码分析

第一种示例方法复杂度解析

空间复杂度

如果输入的元素个数为 n,且 n 大于 10,那么空间复杂度为 O(n);否则,空间复杂度为 O(1)。

所以最坏空间复杂度为 O(n)

时间复杂度

  • 构建二叉堆的过程具有 O(n) 的时间复杂度,其中 n 是输入数组的长度。
  • 接下来,进行 n 次删除操作,每次删除操作的时间复杂度为 O(logn)。由于删除操作会调整堆的结构,保持最大堆的性质,因此每次删除操作的时间复杂度为O(logn)。
  • 因此,总体上,堆排序的时间复杂度为 O(nlogn)

第二种示例方法复杂度解析

空间复杂度

  • heapSort 方法中,除了输入数组之外,没有使用额外的空间。因此,空间复杂度为 O(1),即常数级别的空间复杂度。

时间复杂度:

  • 构建最大堆的过程具有 O(n) 的时间复杂度,其中 n 是输入数组的长度。
  • 接下来,进行 n-1 次堆调整和交换操作,每次操作的时间复杂度为 O(logn)。
  • 因此,总体上,堆排序的时间复杂度为 O(nlogn)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/203583.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

智慧科研助力科研数据的分析处理

如今&#xff0c;科研领域的发展日新月异&#xff0c;数据量也越来越大。这时&#xff0c;智慧科研可视化技术不仅为科研人员提供了快速高效的数据分析手段&#xff0c;而且为科研工作的推进提供了新的思路和方法。通过可视化手段&#xff0c;我们可以将各种数据、信息、知识以…

可行性研究:2023年废旧金属回收行业前景及市场数据分析

废品收购是再生资源行业的重要业务之一。是指将各种废弃物品分类后按不同种类和性能卖给不同的生产厂商或直接出售给再制造厂家&#xff08;如重新使用报废汽车拆解的零件&#xff09;。废旧金属是指暂时失去使用价值的金属或合金制品&#xff0c;一般的废旧金属都含有有用的金…

unity学习笔记12

一、物理系统 如何让一个球体受到重力的影响&#xff1f; 只要给物体添加刚体组件&#xff08;Rigidbody&#xff09;&#xff0c;就可以使其受到重力影响 1.刚体&#xff08;Rigidbody&#xff09;&#xff1a; 刚体是一个组件&#xff0c;用于使游戏对象受到物理引擎的控制。…

Antd可编辑表格初始数据为空,单元格不能编辑的解决办法

黑科技超简单: 给table表格增加行className rowClassName{() > editable-row} 然后设置可编辑表格的行样式 .editable-row:hover .editable-cell-value-wrap {border: 1px solid #d9d9d9;border-radius: 4px;padding: 4px 11px;}.editable-cell-value-wrap {padding: 5px…

网站使用高防CDN进行防护有哪些优势

随着互联网的快速发展&#xff0c;个人网站、企业网站也越来越多&#xff0c;网站之间的竞争也越来越强。网站间越来越大的竞争压力&#xff0c;也时长会衍生一些不法行为&#xff0c;如以攻击为手段&#xff0c;使对方网站陷入瘫痪。针对网站的攻击&#xff0c;市场也是有推出…

【趣味篇】Scratch之管道马里奥

【作品展示】管道马里奥 操作&#xff1a;点击小绿旗进入游戏主页面&#xff0c;通过鼠标控制马里奥左右移动踩在管道上拿到更多金币。

开启虾皮购物新旅程,快速注册买家号

想要在shopee上畅享丰富的购物体验吗&#xff1f;那就让我们一起迈出第一步&#xff0c;注册一个属于你自己的虾皮买家号吧&#xff01; 1. 访问虾皮平台 首先&#xff0c;打开你的浏览器&#xff0c;输入虾皮平台网址&#xff0c;点击注册或登录按钮。这将引导你进入注册界面…

【虚拟机】Docker基础 【一】

1.1.部署MySQL 首先&#xff0c;我们利用Docker来安装一个MySQL软件&#xff0c;大家可以对比一下之前传统的安装方式&#xff0c;看看哪个效率更高一些。 如果是利用传统方式部署MySQL&#xff0c;大概的步骤有&#xff1a; 搜索并下载MySQL安装包上传至Linux环境编译和配置…

Retrobatch for mac图片批处理软件

Retrobatch是一款功能强大的图片批量操作软件&#xff0c;提供了批量加水印、裁剪、压缩等功能&#xff0c;而且处理速度非常快。 在Retrobatch中&#xff0c;用户可以通过拖动相应动作到工作区形成节点(Nodes)&#xff0c;并将节点连接起来形成一个Workflow&#xff0c;最后运…

chrome vue devTools安装

安装好后如下图所示&#xff1a; 一&#xff1a;下载vue devTools 下载链接https://download.csdn.net/download/weixin_44659458/13192207?spm1001.2101.3001.6661.1&utm_mediumdistribute.pc_relevant_t0.none-task-download-2%7Edefault%7ECTRLIST%7EPaid-1-13192207…

Lesson 08 string类 (中)

C&#xff1a;渴望力量吗&#xff0c;少年&#xff1f; 文章目录 二、string类的介绍与使用2. 使用&#xff08;5&#xff09;string类对象的修改操作 三、拷贝1. 引入2. 浅拷贝3. 深拷贝 总结 二、string类的介绍与使用 2. 使用 &#xff08;5&#xff09;string类对象的修改…

网络通信概述

文章目录 IP地址端口号协议三要素作用 五元组协议分层OSI七层模型TCP/IP 五层模型应用层传输层网络层数据链路层物理层 封装和分用发送方 - 封装中间转发接收方 - 分用 一般认为计算机网络就是利用通信线路和通信设备将地理上分散的、具有独立功能的多个计算机系统按不同的形式…

QT学习_16_制作软件安装包

1、准备软件exe及其运行环境 参考&#xff1a;Qt学习_12_一键生成安装包_江湖上都叫我秋博的博客-CSDN博客 这篇博客记录了&#xff0c;如何用window的脚本&#xff0c;一键生成一个可以免安装的软件压缩包&#xff0c;解压缩后&#xff0c;点击exe文件就可以直接运行。 这一…

基于SSM实现的图书管理系统

一、系统架构 前端&#xff1a;jsp | js | css | jquery | layui 后端&#xff1a;spring | springmvc | mybatis 环境&#xff1a;jdk1.7 | mysql | maven | tomcat 二、代码及数据库 三、功能介绍 01. 登录页 02. 首页 03. 借阅管理 04. 图书管理 05. 读者管理 06. 类型管理…

宽量程双计数器测量方法

如要测量计数器信号的数字频率或周期&#xff0c;可使用双计数器方法测量宽量程信号。该方法适于待测量信号的范围较广且整个范围都需要较高测量精度的情况。关于使用宽量程测量方法提高测量精度的详细信息&#xff0c;见量化误差章节。也可使用该方法测量比计数器时基频率更高…

阿里云RDS标准版(x86) vs 经济版(ARM)性能对比

概述 阿里云数据库在去年云栖大会上发布了基于阿里倚天芯片&#xff08;ARM架构&#xff09;的RDS实例&#xff0c;后正式命名为经济版。本文通过标准的sysbench测试&#xff0c;来看看相比与标准版&#xff0c;经济版是否更加“经济”&#xff0c;以帮助开发者选择适合自己的…

什么是API? (应用程序编程接口)

我们经常听到 API 这个专业名称。那么什么是 API 呢&#xff1f; 定义 API&#xff08;Application Programming Interface&#xff0c;应用程序接口&#xff09;是一些预先定义的函数&#xff0c;或指软件系统不同组成部分衔接的约定。目的是提供应用程序与开发人员基于某软…

信号收尾.

sigaction 信号捕捉 它也是信号捕捉&#xff0c;不仅能处理普通信号还能处理实时信号&#xff0c;但我们不管实时信号 我们发现函数名和形参中结构体名一样都是sigaction&#xff0c;这在c/c中允许吗&#xff1f; 不建议&#xff0c;但是可以 signo你要捕捉几号信号 输入型参…

《一带繁花一路歌》趣味化讲述“一带一路”故事,生动化展现“文明互鉴”美好图景

2023年&#xff0c;总台《艺览天下》推出“一带一路”10期特别节目《一带繁花一路歌》&#xff0c;节目邀请“一带一路”沿线国家驻华大使等重量级嘉宾&#xff0c;在地标推介、故事分享、现场互动等环节里趣味化讲述“一带一路”故事&#xff0c;生动地展现出“文明互鉴”的美…

互联网上门家政小程序服务小程序

过去&#xff0c;家政门店主要依靠传统地推方式&#xff0c;如贴广告、发传单、亲友推荐等&#xff0c;被动地等待客户上门。这种方式使得拓展客户的效率低下&#xff0c;而且家政服务市场还存在一些问题&#xff0c;如市场竞争混乱、供需不平衡、员工素质参差不齐等&#xff0…