数据结构之二叉树
在这篇文章里面,我们主要探讨和树相关的话题。
首先,我们来对树进行定义:树是n(n>= 0)个节点的有限集。在任何一个非空树中:(1)有且仅有一个特定的称为“根”的节点;(2)当n>1时,其余节点可分为m(m>0)个互相相关的有限集T1、T2、T3……,其中每一个集合本身又是一棵树,并且称为根的子树。
对于我们这篇文章里讨论的二叉树,它是一种特殊的树形结构,每个节点至多只有两颗子树,并且子树有左右之分,其次序不能随意颠倒。
接下来,我们使用java代码来定义一棵树:

1 public class BinNode { 2 private int m_Value; 3 private BinNode m_Left; 4 private BinNode m_Right; 5 public void setValue(int m_Value) { 6 this.m_Value = m_Value; 7 } 8 public int getValue() { 9 return m_Value; 10 } 11 public void setLeft(BinNode m_Left) { 12 this.m_Left = m_Left; 13 } 14 public BinNode getLeft() { 15 return m_Left; 16 } 17 public void setRight(BinNode m_Right) { 18 this.m_Right = m_Right; 19 } 20 public BinNode getRight() { 21 return m_Right; 22 } 23 24 public boolean isLeaf() 25 { 26 return m_Left == null && m_Right == null; 27 } 28 }
下面,开始讨论和二叉树相关的话题
- 构造二叉树,给出一个已排序的整型数组,如何根据它来构造一个BST(二叉搜索树)。 思路:二叉搜索树的特点是左子树的值<=父节点的值<=右子树的值。我们可以从下标0开始遍历数组,然后依次创建树节点,这样下来,对于树的根节点来说,只有右子树,没有左子树,整个树不是平衡二叉树。为了优化这一点,我们可以将数组的中间元素作为根节点,前半部分的值作为树的左子树,后半部分的值作为树的右子树,然后使用递归依次构建。
1 public static BinNode buildTree(int[] arrValue) 2 { 3 if(arrValue == null) return null; 4 BinNode root = new BinNode(); 5 buildTree(arrValue, 0, arrValue.length - 1, root); 6 return root; 7 } 8 9 private static void buildTree(int[] arrValue, int startPos, int endPos, BinNode tree) 10 { 11 if (startPos > endPos) return; 12 13 int midPos = startPos + (endPos - startPos)/2; 14 if (tree == null) 15 { 16 tree = new BinNode(); 17 } 18 tree.setValue(arrValue[midPos]); 19 20 if (midPos - 1 >= startPos) 21 { 22 BinNode left = new BinNode(); 23 tree.setLeft(left); 24 buildTree(arrValue, startPos, midPos - 1, left); 25 } 26 if (endPos >= midPos + 1) 27 { 28 BinNode right = new BinNode(); 29 tree.setRight(right); 30 buildTree(arrValue, midPos + 1, endPos, right); 31 } 32 }
- 树的遍历(前序遍历、中序遍历、后序遍历、层次遍历) 思路:可以采用递归或者非递归的方式进行遍历 前序遍历
1 public static void preOrder(BinNode tree) 2 { 3 if (tree == null) 4 { 5 return; 6 } 7 System.out.println(tree.getValue()); 8 preOrder(tree.getLeft()); 9 preOrder(tree.getRight()); 10 }
1 public static void preOrder2(BinNode tree) 2 { 3 if (tree == null) return; 4 Stack stack = new Stack(10); 5 BinNode temp = tree; 6 while(temp != null) 7 { 8 System.out.println(temp.getValue()); 9 if (temp.getRight() != null) stack.push(temp.getRight()); 10 temp = temp.getLeft(); 11 } 12 while(stack.get_Count() > 0) 13 { 14 temp = (BinNode)stack.pop(); 15 System.out.println(temp.getValue()); 16 while(temp != null) 17 { 18 if (temp.getRight() != null) 19 { 20 stack.push(temp.getRight()); 21 } 22 temp = temp.getLeft(); 23 } 24 } 25 }
中序遍历
1 public static void inOrder(BinNode tree) 2 { 3 if (tree == null) 4 { 5 return; 6 } 7 inOrder(tree.getLeft()); 8 System.out.println(tree.getValue()); 9 inOrder(tree.getRight()); 10 }
1 public static void inOrder2(BinNode tree) 2 { 3 if (tree == null) return; 4 Stack stack = new Stack(10); 5 BinNode temp = tree; 6 while(temp != null) 7 { 8 stack.push(temp); 9 temp = temp.getLeft(); 10 } 11 while(stack.get_Count() > 0) 12 { 13 temp = (BinNode)stack.pop(); 14 System.out.println(temp.getValue()); 15 if (temp.getRight() != null) 16 { 17 temp = temp.getRight(); 18 stack.push(temp); 19 while(temp != null) 20 { 21 if (temp.getLeft() != null) 22 { 23 stack.push(temp.getLeft()); 24 } 25 temp = temp.getLeft(); 26 } 27 } 28 } 29 }
后序遍历
1 public static void postOrder(BinNode tree) 2 { 3 if (tree == null) 4 { 5 return; 6 } 7 postOrder(tree.getLeft()); 8 postOrder(tree.getRight()); 9 System.out.println(tree.getValue()); 10 }
1 public static void postOrder2(BinNode tree) 2 { 3 if (tree == null) return; 4 Stack stack = new Stack(10); 5 BinNode temp = tree; 6 while(temp != null) 7 { 8 stack.push(temp); 9 temp = temp.getLeft(); 10 } 11 12 while(stack.get_Count() > 0) 13 { 14 BinNode lastVisited = temp; 15 temp = (BinNode)stack.pop(); 16 if (temp.getRight() == null || temp.getRight() == lastVisited) 17 { 18 System.out.println(temp.getValue()); 19 } 20 else if (temp.getLeft() == lastVisited) 21 { 22 stack.push(temp); 23 temp = temp.getRight(); 24 stack.push(temp); 25 while(temp != null) 26 { 27 if (temp.getLeft() != null) 28 { 29 stack.push(temp.getLeft()); 30 } 31 temp = temp.getLeft(); 32 } 33 } 34 } 35 }
层次遍历
1 public static void printTree(BinNode tree) 2 { 3 if (tree == null) 4 { 5 return; 6 } 7 Queue queue = new Queue(10); 8 queue.enQueue(tree); 9 while(queue.get_Count() > 0) 10 { 11 BinNode temp = (BinNode) queue.deQueue(); 12 System.out.println(temp.getValue()); 13 if (temp.getLeft() != null) queue.enQueue(temp.getLeft()); 14 if (temp.getRight() != null) queue.enQueue(temp.getRight()); 15 } 16 }
- 判断二叉树是否是平衡二叉树 思路:平衡二叉树的特点是左右子树的深度差不能大于1,采用递归的方式。
1 public static boolean isBalance(BinNode tree) 2 { 3 if (tree == null) 4 { 5 return true; 6 } 7 int lDepth = getDepth(tree.getLeft()); 8 int rDepth = getDepth(tree.getRight()); 9 if (lDepth - rDepth > 1 || lDepth - rDepth < -1) 10 { 11 return false; 12 } 13 return isBalance(tree.getRight()) && isBalance(tree.getLeft()); 14 } 15 16 17 public static int getDepth(BinNode tree) 18 { 19 if(tree == null) 20 { 21 return 0; 22 } 23 int lLength = getDepth(tree.getLeft()); 24 int rLength = getDepth(tree.getRight()); 25 26 return lLength > rLength ? lLength + 1 : rLength + 1; 27 }
- 找出任何两个节点的最近的共同父节点 思路:用1表示根节点,2、3表示第二层节点,4、5、6、7表示第三层节点,以此类推,对于编号为n的节点,它的子节点编号为(2*n和2*n+1),通过这种方式重新标记树后,我们分别找到两个节点对应的编号,然后分别进行除2操作,直到找到相同的节点,这个节点就是最近的共同父节点。
1 public static BinNode findParentNode2(BinNode tree, BinNode node1, BinNode node2) 2 { 3 if (tree == null) return null; 4 BinNode result = null; 5 int pos1 = 1, pos2 = 1; 6 int i = 1; 7 java.util.HashMap<Integer, BinNode> table = new java.util.HashMap<Integer, BinNode>(); 8 BinNode temp = tree; 9 table.put(i, temp); 10 Queue queue = new Queue(10); 11 queue.enQueue(i); 12 int count = 0; 13 while(queue.get_Count() > 0) 14 { 15 i = Integer.valueOf(queue.deQueue().toString()); 16 if (table.get(i) != null && table.get(i).getValue() == node1.getValue()) 17 { 18 pos1 = i; 19 count++; 20 } 21 if (table.get(i) != null && table.get(i).getValue() == node2.getValue()) 22 { 23 pos2 = i; 24 count++; 25 } 26 if(count == 2) break; 27 28 if(table.get(i) == null) 29 { 30 table.put(2*i, null); 31 table.put(2*i + 1, null); 32 } 33 else 34 { 35 table.put(2*i, table.get(i).getLeft()); 36 table.put(2*i + 1, table.get(i).getRight()); 37 } 38 queue.enQueue(2*i); 39 queue.enQueue(2*i + 1); 40 41 } 42 if (pos1 == pos2) 43 { 44 result = table.get(pos1); 45 } 46 else if(pos1 > pos2) 47 { 48 while(pos1 > pos2) pos1 = pos1/2; 49 } 50 else 51 { 52 while(pos2 > pos1) pos2 = pos2/2; 53 } 54 if(pos1 == pos2 || pos1/2 == pos2 || pos2/2 == pos1) 55 { 56 result = table.get(pos1 > pos2 ? pos2 : pos1); 57 } 58 else 59 { 60 while(pos1 != pos2) 61 { 62 pos1 = pos1/2; 63 pos2 = pos2/2; 64 } 65 } 66 result = table.get(pos1); 67 68 return result; 69 }
如果我们已知树是二叉排序树,那么我们可以利用二叉排序树的特点,分别寻找两个节点,在寻找过程中,将扫描过的节点放入stack中,这样可以获得两个stack,然后分别对两个stack进行pop操作,得到的第一个相同的节点就是最近的共同父节点。
1 public static BinNode findParentNode(BinNode tree, BinNode node1, BinNode node2) 2 { 3 if (tree == null) 4 { 5 return null; 6 } 7 8 BinNode result = null; 9 Stack stack1 = new Stack(10); 10 Stack stack2 = new Stack(10); 11 stack1.push(tree); 12 stack2.push(tree); 13 BinNode temp = tree; 14 while(temp != null && temp.getValue() != node1.getValue()) 15 { 16 stack1.push(temp); 17 if (temp.getValue()> node1.getValue()) 18 { 19 temp = temp.getLeft(); 20 } 21 else 22 { 23 temp = temp.getRight(); 24 } 25 } 26 temp = tree; 27 while(temp != null && temp.getValue() != node2.getValue()) 28 { 29 stack2.push(temp); 30 if (temp.getValue()> node2.getValue()) 31 { 32 temp = temp.getLeft(); 33 } 34 else 35 { 36 temp = temp.getRight(); 37 } 38 } 39 if (stack1.get_Count() > stack2.get_Count()) 40 { 41 while(stack1.get_Count() > stack2.get_Count()) 42 { 43 stack1.pop(); 44 } 45 } 46 if (stack2.get_Count() > stack1.get_Count()) 47 { 48 while(stack2.get_Count()>stack1.get_Count()) 49 { 50 stack2.pop(); 51 } 52 } 53 while(stack1.get_Count() > 0 && stack2.get_Count() > 0) 54 { 55 if (stack1.peek() == stack2.peek()) 56 { 57 result = (BinNode)stack1.peek(); 58 break; 59 } 60 stack1.pop(); 61 stack2.pop(); 62 } 63 64 return result; 65 }
- 找出树中节点值得和为某一值得所有路径 思路:肯定是需要使用递归来实现,使用stack来存储扫描过的路径。
1 public static void findSum(BinNode tree, int sum, Stack stack) 2 { 3 if (tree == null) return; 4 stack.push(tree); 5 if (tree.getValue() == sum) 6 { 7 Stack temp = new Stack(stack.get_Count()); 8 while(stack.get_Count()>0) 9 { 10 temp.push(stack.pop()); 11 } 12 StringBuffer sb = new StringBuffer(); 13 while(temp.get_Count() > 0) 14 { 15 sb.append(((BinNode)temp.peek()).getValue()).append("->"); 16 stack.push(temp.pop()); 17 } 18 System.out.println(sb.substring(0, sb.length() - 2)); 19 } 20 else if (tree.getValue() < sum) 21 { 22 if (tree.getLeft() != null) findSum(tree.getLeft(), sum - tree.getValue(), stack); 23 if (tree.getRight() != null) findSum(tree.getRight(), sum - tree.getValue(), stack); 24 } 25 stack.pop(); 26 }
- 判断给定数组是否是二叉搜索树后序遍历的结果 思路:观察二叉搜索树后序遍历的特点,根节点在最后,从下标0开始扫描,直到第一个大于根节点值得下标m,[0,(m-1)]的元素是节点的左子树,[m, n]是节点的右子树,并且右子树中任何节点的值都大于根节点。这样可以使用递归的方式以此判断。
-
相传此乃网易二面题
要求现场手写代码判断一个数字序列是BST后序遍历的结果。
如图所示 对于后续遍历的排序二叉树应具有上面的一个不等式性质 以及 根节点为最后一个节点的性质
因此代码如下
- //序列array有n个元素
- //如果这n个元素是二叉排序树的后续遍历结果 返回true
- //否则 返回false
- bool isBST(int * array, size_t n)
- {
- assert(array != NULL);
- if (n <= 1)
- {
- return true; // 有一个节点 或0个节点 符合树的性质
- }
- size_t i = 0;
- for (i = 0; i < n-1; ++i)
- {
- if (array[i] > array[n-1]) //array[n-1] 为数组最后一个元素 如果是后续遍历 应该是树的根节点。根节点应该大于左子树中的所有节点
- {
- break;
- }
- }
- if (i == n-1)
- {
- //全是左子树中的点
- return isBST(array, n-1); //判断左子树是否成立
- }
- size_t l_end = i;
- for (i; i < n-1; ++i)
- {
- if (array[i] < array[n-1])
- {
- break; //右子树中的所有节点应该大于根节点
- }
- }
- if (i == n-1)
- {
- // 判断左右子树是否合法
- return isBST(array, l_end) && isBST(array+l_end, n - 1 - l_end);
- }
- return false;
- }
//序列array有n个元素 //如果这n个元素是二叉排序树的后续遍历结果 返回true //否则 返回false bool isBST(int * array, size_t n) { assert(array != NULL); if (n <= 1) { return true; // 有一个节点 或0个节点 符合树的性质 } size_t i = 0; for (i = 0; i < n-1; ++i) { if (array[i] > array[n-1]) //array[n-1] 为数组最后一个元素 如果是后续遍历 应该是树的根节点。根节点应该大于左子树中的所有节点 { break; } } if (i == n-1) { //全是左子树中的点 return isBST(array, n-1); //判断左子树是否成立 } size_t l_end = i; for (i; i < n-1; ++i) { if (array[i] < array[n-1]) { break; //右子树中的所有节点应该大于根节点 } } if (i == n-1) { // 判断左右子树是否合法 return isBST(array, l_end) && isBST(array+l_end, n - 1 - l_end); } return false; }
测试如下的二叉排序树
- void test_isBST()
- {
- int a[]={0};
- int *p;
- size_t n = sizeof(a)/sizeof(a[0]);
- p=a;
- n = sizeof(a)/sizeof(a[0]);
- assert(isBST(p,n) == true);
- assert(isBST(p,0) == true);
- int b[]={2,4,3,7,6,8,5};
- p=b;
- n = sizeof(b)/sizeof(b[0]);
- assert(isBST(p,n) == true);
- //int c[]={2,4,3,8,6,7,5};
- int c[]={8,6,7};
- p=c;
- n = sizeof(c)/sizeof(c[0]);
- assert(isBST(p,n) == false);
- }
- 给定一颗BST,另f=(max+min)/2,寻找距离f最近但大于f的节点 思路:对于BST,最小值是最左边的叶子节点,最大值是最右边的叶子节点。
1 public static BinNode findAvgInBST(BinNode tree) 2 { 3 if (tree == null) return null; 4 BinNode temp = tree; 5 while(temp.getLeft() != null) temp = temp.getLeft(); 6 int minValue = temp.getValue(); 7 temp = tree; 8 while(temp.getRight() != null) temp = temp.getRight(); 9 int maxValue = temp.getValue(); 10 int avgValue = (minValue + maxValue)/2; 11 return findNode(tree, avgValue); 12 } 13 14 private static BinNode findNode(BinNode tree, int value) 15 { 16 if (tree == null) return null; 17 if (tree.getValue() >= value) 18 { 19 if (tree.getLeft() == null) 20 { 21 return tree; 22 } 23 else 24 { 25 BinNode temp = findNode(tree.getLeft(), value); 26 return temp.getValue() < tree.getValue() ? temp:tree; 27 } 28 } 29 else 30 { 31 if (tree.getRight() == null) 32 { 33 return tree; 34 } 35 else 36 { 37 BinNode temp = findNode(tree.getRight(), value); 38 return temp.getValue() < tree.getValue() ? temp:tree; 39 } 40 } 41 }
- 给出一棵树,得到这棵树的镜像 思路:我们可以考虑前序遍历的结果:父节点->左子树->右子树,而镜像之后的结果:父节点->右子树->左子树,我们可以采用递归前序遍历的方式来实现
1 public static void imageTree(BinNode tree) 2 { 3 if (tree == null) return; 4 BinNode temp = tree.getLeft(); 5 tree.setLeft(tree.getRight()); 6 tree.setRight(temp); 7 if (tree.getLeft() != null) imageTree(tree.getLeft()); 8 if (tree.getRight() != null) imageTree(tree.getRight()); 9 }
我们也可以采用循环+栈的方式来实现
1 public static void imageTrees(BinNode tree) 2 { 3 if (tree == null) return; 4 Stack stack = new Stack(10); 5 BinNode temp = tree; 6 stack.push(temp); 7 while(stack.get_Count() > 0) 8 { 9 temp = (BinNode)stack.pop(); 10 BinNode temp1 = temp.getLeft(); 11 temp.setLeft(temp.getRight()); 12 temp.setRight(temp1); 13 if (temp.getLeft() != null) 14 { 15 stack.push(temp.getLeft()); 16 } 17 if (temp.getRight() != null) 18 { 19 stack.push(temp.getRight()); 20 } 21 } 22 }
- 将BST转换为排序的双向链表 思路:对于BST来说,中序遍历的结果就是元素按照从小到大进行排序的结果
1 public static DoubleLink convertTreeToList(BinNode tree) 2 { 3 if (tree == null) return null; 4 5 DoubleLink list = new DoubleLink(); 6 list.setValue(null); 7 list.setPre(null); 8 DoubleLink listNode = list; 9 10 BinNode treeNode = tree; 11 Stack stack = new Stack(10); 12 13 while(treeNode != null) 14 { 15 stack.push(treeNode); 16 treeNode = treeNode.getLeft(); 17 } 18 19 while(stack.get_Count() > 0) 20 { 21 treeNode = (BinNode)stack.pop(); 22 DoubleLink tempListNode = new DoubleLink(); 23 tempListNode.setValue(treeNode); 24 listNode.setNext(tempListNode); 25 tempListNode.setPre(listNode); 26 listNode = tempListNode; 27 if (treeNode.getRight() != null) 28 { 29 treeNode = treeNode.getRight(); 30 stack.push(treeNode); 31 while(treeNode != null) 32 { 33 if (treeNode.getLeft() != null) 34 { 35 stack.push(treeNode.getLeft()); 36 } 37 treeNode = treeNode.getLeft(); 38 } 39 } 40 } 41 42 listNode.setNext(null); 43 44 return list; 45 }
其中DoubleLink的定义如下
1 public class DoubleLink { 2 private Object value; 3 private DoubleLink next; 4 private DoubleLink pre; 5 public void setValue(Object value) { 6 this.value = value; 7 } 8 public Object getValue() { 9 return value; 10 } 11 public void setNext(DoubleLink next) { 12 this.next = next; 13 } 14 public DoubleLink getNext() { 15 return next; 16 } 17 public void setPre(DoubleLink pre) { 18 this.pre = pre; 19 } 20 public DoubleLink getPre() { 21 return pre; 22 } 23 }
- 向BST中插入节点 思路:对BST中插入节点,首先按照遍历BST,找出所插接点的父节点,然后创建新节点
1 public static void addValueToBST(BinNode tree, int value) 2 { 3 if (tree == null) 4 { 5 tree = new BinNode(); 6 tree.setValue(value); 7 return; 8 } 9 10 BinNode temp = tree; 11 while(true) 12 { 13 if (temp.getValue() >= value) 14 { 15 if (temp.getLeft() == null) break; 16 temp = temp.getLeft(); 17 } 18 else 19 { 20 if (temp.getRight() == null) break; 21 temp = temp.getRight(); 22 } 23 } 24 25 BinNode node = new BinNode(); 26 node.setValue(value); 27 if (temp.getValue() >= value) 28 { 29 temp.setLeft(node); 30 } 31 else 32 { 33 temp.setRight(node); 34 } 35 }
- 向BST中删除节点 思路:首先是要能够找到需要删除的节点,然后根据节点是否有子节点分情况处理,如果没有子树,那么直接删除该节点(从父节点上解除关联);如果只有左子树或者右子树,那么删除节点后将左子树或者右子树直接挂在父节点下;如果左右子树均存在,那么需要找出左子树的最大值或者右子树的最小值作为父节点的直接子节点,然后将这个节点的左右子树分别设置为删除节点的左右子树。
1 public static void delValueFromBST(BinNode tree, int value) 2 { 3 if (tree == null) return; 4 BinNode deleteNode = findNodeInBST(tree, value); 5 BinNode parentNode = findParentNodeInBST(tree, value); 6 if (parentNode == null || deleteNode == null) return; 7 if (deleteNode.isLeaf()) 8 { 9 if (parentNode.getLeft() == deleteNode) parentNode.setLeft(null); 10 else parentNode.setRight(null); 11 } 12 else if (deleteNode.getLeft() == null && deleteNode.getRight() != null) 13 { 14 if (parentNode.getValue() >= deleteNode.getRight().getValue()) parentNode.setLeft(deleteNode.getRight()); 15 else parentNode.setRight(deleteNode.getRight()); 16 deleteNode.setRight(null); 17 } 18 else if (deleteNode.getLeft() != null && deleteNode.getRight() == null) 19 { 20 if (parentNode.getValue() >= deleteNode.getLeft().getValue()) parentNode.setLeft(deleteNode.getLeft()); 21 else parentNode.setRight(deleteNode.getLeft()); 22 deleteNode.setLeft(null); 23 } 24 else 25 { 26 BinNode temp = deleteNode.getRight(); 27 BinNode parent = deleteNode.getRight(); 28 while(temp.getLeft() != null) 29 { 30 parent = temp; 31 temp = temp.getLeft(); 32 } 33 if (parent != temp) 34 { 35 parent.setLeft(null); 36 } 37 temp.setLeft(deleteNode.getLeft()); 38 temp.setRight(deleteNode.getRight()); 39 if (parentNode.getValue() >= temp.getValue()) parentNode.setLeft(temp); 40 else parentNode.setRight(temp); 41 deleteNode.setLeft(null); 42 deleteNode.setRight(null); 43 } 44 } 45 46 private static BinNode findNodeInBST(BinNode tree, int value) 47 { 48 if (tree == null) return null; 49 if (tree.getValue() == value) return tree; 50 else if (tree.getValue() > value) return findNodeInBST(tree.getLeft(), value); 51 else return findNodeInBST(tree.getRight(), value); 52 } 53 54 private static BinNode findParentNodeInBST(BinNode tree, int value) 55 { 56 if (tree == null) return null; 57 if (tree.getValue() == value) return null; 58 if ((tree.getLeft() != null && tree.getLeft().getValue() == value) || 59 (tree.getRight() != null &&tree.getRight().getValue() == value)) return tree; 60 else if (tree.getValue() > value) return findParentNodeInBST(tree.getLeft(), value); 61 else return findParentNodeInBST(tree.getRight(), value); 62 }
最后,欢迎大家提出更多和二叉树相关的面试题目,我们可以一起讨论。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!