当前位置: 首页 > 编程日记 > 正文

Java编程的逻辑 (39) - 剖析LinkedList

本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http://item.jd.com/12299018.html


上节我们介绍了ArrayList,ArrayList随机访问效率很高,但插入和删除性能比较低,我们提到了同样实现了List接口的LinkedList,它的特点与ArrayList几乎正好相反,本节我们就来详细介绍LinkedList。

除了实现了List接口外,LinkedList还实现了Deque和Queue接口,可以按照队列、栈和双端队列的方式进行操作,本节会介绍这些用法,同时介绍其实现原理。

我们先来看它的用法。

用法

构造方法

LinkedList的构造方法与ArrayList类似,有两个,一个是默认构造方法,另外一个可以接受一个已有的Collection,如下所示:

public LinkedList()
public LinkedList(Collection<? extends E> c)

比如,可以这么创建:

List<String> list = new LinkedList<>();
List<String> list2 = new LinkedList<>(Arrays.asList(new String[]{"a","b","c"}));

List接口

LinkedList与ArrayList一样,同样实现了List接口,而List接口扩展了Collection接口,Collection又扩展了Iterable接口,所有这些接口的方法都是可以使用的,使用方法与上节介绍的一样,本节就不再赘述了。

队列 (Queue)

LinkedList还实现了队列接口Queue,所谓队列就类似于日常生活中的各种排队,特点就是先进先出,在尾部添加元素,从头部删除元素,它的接口定义为:

public interface Queue<E> extends Collection<E> {boolean add(E e);boolean offer(E e);E remove();E poll();E element();E peek();
} 

Queue扩展了Collection,它的主要操作有三个:

  • 在尾部添加元素 (add, offer)
  • 查看头部元素 (element, peek),返回头部元素,但不改变队列
  • 删除头部元素 (remove, poll),返回头部元素,并且从队列中删除

每种操作都有两种形式,有什么区别呢?区别在于,对于特殊情况的处理不同。特殊情况是指,队列为空或者队列为满,为空容易理解,为满是指队列有长度大小限制,而且已经占满了。LinkedList的实现中,队列长度没有限制,但别的Queue的实现可能有。

在队列为空时,element和remove会抛出异常NoSuchElementException,而peek和poll返回特殊值null,在队列为满时,add会抛出异常IllegalStateException,而offer只是返回false。

把LinkedList当做Queue使用也很简单,比如,可以这样:

Queue<String> queue = new LinkedList<>();queue.offer("a");
queue.offer("b");
queue.offer("c");while(queue.peek()!=null){System.out.println(queue.poll());    
}

输出为:

a
b
c

我们在介绍函数调用原理的时候介绍过栈,栈也是一种常用的数据结构,与队列相反,它的特点是先进后出、后进先出,类似于一个储物箱,放的时候是一件件往上放,拿的时候则只能从上面开始拿。

Java中有一个类Stack,用于表示栈,但这个类已经过时了,我们不再介绍,Java中没有单独的栈接口,栈相关方法包括在了表示双端队列的接口Deque中,主要有三个方法:

void push(E e);
E pop();
E peek();

解释下:

  • push表示入栈,在头部添加元素,栈的空间可能是有限的,如果栈满了,push会抛出异常IllegalStateException。
  • pop表示出栈,返回头部元素,并且从栈中删除,如果栈为空,会抛出异常NoSuchElementException。
  • peek查看栈头部元素,不修改栈,如果栈为空,返回null。

把LinkedList当做栈使用也很简单,比如,可以这样:

Deque<String> stack = new LinkedList<>();stack.push("a");
stack.push("b");
stack.push("c");while(stack.peek()!=null){System.out.println(stack.pop());    
}

输出为:

c
b
a

双端队列 (Deque)

栈和队列都是在两端进行操作,栈只操作头部,队列两端都操作,但尾部只添加、头部只查看和删除,有一个更为通用的操作两端的接口Deque,Deque扩展了Queue,包括了栈的操作方法,此外,它还有如下更为明确的操作两端的方法:

void addFirst(E e);
void addLast(E e);
E getFirst();
E getLast();
boolean offerFirst(E e);
boolean offerLast(E e);
E peekFirst();
E peekLast();
E pollFirst();
E pollLast();
E removeFirst();
E removeLast();

xxxFirst操作头部,xxxLast操作尾部。与队列类似,每种操作有两种形式,区别也是在队列为空或满时,处理不同。为空时,getXXX/removeXXX会抛出异常,而peekXXX/pollXXX会返回null。队列满时,addXXX会抛出异常,offerXXX只是返回false。

栈和队列只是双端队列的特殊情况,它们的方法都可以使用双端队列的方法替代,不过,使用不同的名称和方法,概念上更为清晰。

Deque接口还有一个迭代器方法,可以从后往前遍历

Iterator<E> descendingIterator();

比如,看如下代码:

Deque<String> deque = new LinkedList<>(Arrays.asList(new String[]{"a","b","c"}));
Iterator<String> it = deque.descendingIterator();
while(it.hasNext()){System.out.print(it.next()+" ");
}

输出为

c b a 

用法小结

LinkedList的用法是比较简单的,与ArrayList用法类似,支持List接口,只是,LinkedList增加了一个接口Deque,可以把它看做队列、栈、双端队列,方便的在两端进行操作。

如果只是用作List,那应该用ArrayList还是LinkedList呢?我们需要了解下LinkedList的实现原理。

实现原理

内部组成

我们知道,ArrayList内部是数组,元素在内存是连续存放的,但LinkedList不是。LinkedList直译就是链表,确切的说,它的内部实现是双向链表,每个元素在内存都是单独存放的,元素之间通过链接连在一起,类似于小朋友之间手拉手一样。

为了表示链接关系,需要一个节点的概念,节点包括实际的元素,但同时有两个链接,分别指向前一个节点(前驱)和后一个节点(后继),节点是一个内部类,具体定义为:

private static class Node<E> {E item;Node<E> next;Node<E> prev;Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}
}

Node类表示节点,item指向实际的元素,next指向下一个节点,prev指向前一个节点。

LinkedList内部组成就是如下三个实例变量:

transient int size = 0;
transient Node<E> first;
transient Node<E> last;

我们暂时忽略transient关键字,size表示链表长度,默认为0,first指向头节点,last指向尾节点,初始值都为null。

LinkedList的所有public方法内部操作的都是这三个实例变量,具体是怎么操作的?链接关系是如何维护的?我们看一些主要的方法,先来看add方法。

Add方法

add方法的代码为:

public boolean add(E e) {linkLast(e);return true;
}

主要就是调用了linkLast,它的代码为:

void linkLast(E e) {final Node<E> l = last;final Node<E> newNode = new Node<>(l, e, null);last = newNode;if (l == null)first = newNode;elsel.next = newNode;size++;modCount++;
}

代码的基本步骤是:

1. 创建一个新的节点newNode。prev指向原来的尾节点,如果原来链表为空,则为null。代码为:

Node<E> newNode = new Node<>(l, e, null);

2. 修改尾节点last,指向新的最后节点newNode。代码为:

last = newNode;

3. 修改前节点的后向链接,如果原来链表为空,则让头节点指向新节点,否则让前一个节点的next指向新节点。代码为:

if (l == null)first = newNode;
elsel.next = newNode;

4. 增加链表大小。代码为:

size++

modCount++的目的与ArrayList是一样的,记录修改次数,便于迭代中间检测结构性变化。

我们通过一些图示来更清楚的看一下,比如说,代码为:

List<String> list = new LinkedList<String>();
list.add("a");
list.add("b");

执行完第一行后,内部结构如下所示:


添加完"a"后,内部结构如下所示:


添加完"b"后,内部结构如下所示:

可以看出,与ArrayList不同,LinkedList的内存是按需分配的,不需要预先分配多余的内存,添加元素只需分配新元素的空间,然后调节几个链接即可。

根据索引访问元素 get

添加了元素,如果根据索引访问元素呢?我们看下get方法的代码:

public E get(int index) {checkElementIndex(index);return node(index).item;
}

checkElementIndex检查索引位置的有效性,如果无效,抛出异常,代码为:

private void checkElementIndex(int index) {if (!isElementIndex(index))throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}private boolean isElementIndex(int index) {return index >= 0 && index < size;
}

如果index有效,则调用node方法查找对应的节点,其item属性就指向实际元素内容,node方法的代码为:

Node<E> node(int index) {if (index < (size >> 1)) {Node<E> x = first;for (int i = 0; i < index; i++)x = x.next;return x;} else {Node<E> x = last;for (int i = size - 1; i > index; i--)x = x.prev;return x;}
}

size>>1等于size/2,如果索引位置在前半部分 (index<(size>>1)),则从头节点开始查找,否则,从尾节点开始查找。

可以看出,与ArrayList明显不同,ArrayList中数组元素连续存放,可以直接随机访问,而在LinkedList中,则必须从头或尾,顺着链接查找,效率比较低。

根据内容查找元素

我们看下indexOf的代码:

public int indexOf(Object o) {int index = 0;if (o == null) {for (Node<E> x = first; x != null; x = x.next) {if (x.item == null)return index;index++;}} else {for (Node<E> x = first; x != null; x = x.next) {if (o.equals(x.item))return index;index++;}}return -1;
}

代码也很简单,从头节点顺着链接往后找,如果要找的是null,则找第一个item为null的节点,否则使用equals方法进行比较。

插入元素

add是在尾部添加元素,如果在头部或中间插入元素呢?可以使用如下方法:

public void add(int index, E element)

它的代码是:

public void add(int index, E element) {checkPositionIndex(index);if (index == size)linkLast(element);elselinkBefore(element, node(index));
}

如果index为size,添加到最后面,一般情况,是插入到index对应节点的前面,调用方法为linkBefore,它的代码为:

void linkBefore(E e, Node<E> succ) {final Node<E> pred = succ.prev;final Node<E> newNode = new Node<>(pred, e, succ);succ.prev = newNode;if (pred == null)first = newNode;elsepred.next = newNode;size++;modCount++;
}

参数succ表示后继节点。变量pred就表示前驱节点。目标就是在pred和succ中间插入一个节点。插入步骤是:

1. 新建一个节点newNode,前驱为pred,后继为succ。代码为:

Node<E> newNode = new Node<>(pred, e, succ);

2. 让后继的前驱指向新节点。代码为:

succ.prev = newNode;

3. 让前驱的后继指向新节点,如果前驱为空,修改头节点指向新节点。代码为:

if (pred == null)first = newNode;
elsepred.next = newNode;

4. 增加长度。

我们通过图示来更清楚的看下,还是上面的例子,比如,添加一个元素:

list.add(1, "c");

图示结构会变为:

可以看出,在中间插入元素,LinkedList只需按需分配内存,修改前驱和后继节点的链接,而ArrayList则可能需要分配很多额外空间,且移动所有后续元素。

删除元素

我们再来看删除元素,代码为:

public E remove(int index) {checkElementIndex(index);return unlink(node(index));
}

通过node方法找到节点后,调用了unlink方法,代码为:

E unlink(Node<E> x) {final E element = x.item;final Node<E> next = x.next;final Node<E> prev = x.prev;if (prev == null) {first = next;} else {prev.next = next;x.prev = null;}if (next == null) {last = prev;} else {next.prev = prev;x.next = null;}x.item = null;size--;modCount++;return element;
}

删除x节点,基本思路就是让x的前驱和后继直接链接起来,next是x的后继,prev是x的前驱,具体分为两步:

  1. 第一步是让x的前驱的后继指向x的后继。如果x没有前驱,说明删除的是头节点,则修改头节点指向x的后继。
  2. 第二步是让x的后继的前驱指向x的前驱。如果x没有后继,说明删除的是尾节点,则修改尾节点指向x的前驱。

我们再通过图示看下,还是上面的例子,如果删除一个元素:

list.remove(1);

图示结构会变为:

原理小结

以上,我们介绍了LinkedList的内部组成,以及几个主要方法的实现代码,其他方法的原理也都类似,我们就不赘述了。

前面我们提到,对于队列、栈和双端队列接口,长度可能有限制,LinkedList实现了这些接口,不过LinkedList对长度并没有限制。

LinkedList特点分析

LinkedList内部是用双向链表实现的,维护了长度、头节点和尾节点,这决定了它有如下特点:

  • 按需分配空间,不需要预先分配很多空间
  • 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)。
  • 不管列表是否已排序,只要是按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)。
  • 在两端添加、删除元素的效率很高,为O(1)。
  • 在中间插入、删除元素,要先定位,效率比较低,为O(N),但修改本身的效率很高,效率为O(1)。

理解了LinkedList和ArrayList的特点,我们就能比较容易的进行选择了,如果列表长度未知,添加、删除操作比较多,尤其经常从两端进行操作,而按照索引位置访问相对比较少,则LinkedList就是比较理想的选择。

小结
本节详细介绍了LinkedList,先介绍了用法,然后介绍了实现原理,最后我们分析了LinkedList的特点,并与ArrayList进行了比较。

用法上,LinkedList是一个List,但也实现了Deque接口,可以作为队列、栈和双端队列使用。实现原理上,内部是一个双向链表,并维护了长度、头节点和尾节点。

无论是ArrayList还是LinkedList,按内容查找元素的效率都很低,都需要逐个进行比较,有没有更有效的方式呢?

----------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

相关文章:

运用.NET读写Windows注册编辑表

作者&#xff1a; 冉林仓 www.ASPCool.com 时间:2001-11-9 如果你曾经使用过RegOpenKeyEx、RegCreateKeyEx、RegCloseKey等Win32 API函数读写过注册编辑表&#xff0c;你肯定非常熟悉这些复杂的Registry函数。相反&#xff0c;在.NET框架中&#xff0c;Registry和RegistryK…

使用正则表达式抽取新闻/BBS网页发表时间

package org.apache.nutch.parse.html; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 分析时间戳 * * author xum * */ public class Publish…

为什么Python没有main函数?

作者 | 豌豆花下猫来源 | Python猫&#xff08;ID:python_cat&#xff09;众所周知&#xff0c;Python中没有所谓的main函数&#xff0c;但是网上经常有文章提到“ Python的main函数”和“建议编写main函数”。其实&#xff0c;可能他们是想模仿真正的main函数&#xff0c;但是…

HTTP访问服务的相关解释

一、访问网站的基本流程第一步&#xff1a;客户端用户在浏览器输入www.51cto.com网站&#xff0c;回车后&#xff0c;系统首先会查找系统本地的DNS缓存及hosts文件信息&#xff0c;确定是否存在www.51cto.com余名对应的IP解析记录&#xff0c;如果有就直接获取IP地址&#xff0…

关于ASP.Net中的时间处理

作者&#xff1a; 飞刀 www.ASPCool.com 时间:2001-8-8 这里我想谈谈ASP.Net中对时间的处理 在ASP.Net中&#xff0c;M$为我们提供一种名为DateTime的对象&#xff0c;我们用这个对象来取得当前的时间。比如&#xff1a; DateTime dtDateTime.Now; 在上面…

还缺30万人!程序员2020年要过好日子了……

最近&#xff0c;程序员届有一个重大好消息&#xff0c;可能很多人还不知道&#xff0c;那就是&#xff1a;国内某些城市已经开始程序员人才补贴了&#xff01;对于人工智能公司的项目开发、人才引进、科技研发&#xff0c;最高按照国拨经费的30%给予配套支持&#xff0c;单个项…

淘宝海量数据库之二:一致性选择

众所周知&#xff0c;一致性是数据最关键的属性之一。2000年&#xff0c;Eric Brewer教授在ACM分布式计算年会上指出了著名的CAP理论&#xff1a; Brewer, E. A. 2000. Towards robust distributed systems. In Proceedings of the 19th Annual ACM Symposium on Principles of…

Linux 小记录!

rmdir与 rm -r 的不同处前者这能删除目录 后者目录和文件都可以删除cp 和echo 都会覆盖原有的内容ctrl &#xff0b; c 强制中断这条命令/前后是没有空格的快捷键&#xff1a;TAB 命令 路径补全符号&#xff1a;; 多个命令的分隔符/ 根或者路径的分隔符。> 标准输出重定向…

Session 详解

作者&#xff1a; heallven www.ASPCool.com 时间:2004-8-28 阅读本文章之前的准备 阅读本文章前&#xff0c;需要读者对以下知识有所了解。否则&#xff0c;阅读过程中会在相应的内容上遇到不同程度的问题。 懂得ASP/ASP.NET编程 了解ASP/ASP.NET的S…

实现一个模拟CMD.exe命令编辑模式执行与显示的Delphi控件

cmd.exe这个东西是Windows系统自带的执行Dos的一个灰常好的人机命令交互的执行方式&#xff0c;现在很多脚本语言也都带有这种即时解释的人机模式。当下由于工程的需要&#xff0c;也要做一个类似命令解释显示的编辑器&#xff0c;基本上完全模拟Cmd.exe的这种交互模式&#xf…

谷歌这波大动作,暴露了什么信号?

我们都知道谷歌爸爸收购了Cask Data一家公司。长期以来&#xff0c;谷歌致力于推动围绕 GoogleCloud 的企业业务&#xff0c;但在这方面一直被亚马逊和微软吊打&#xff0c;这次的收购正是为了弥补自身的短板。被收购的 Cask Data 是一家专门提供基于Hadoop的大型数据分析服务解…

OSChina 周一乱弹 ——喝不到放心奶

2019独角兽企业重金招聘Python工程师标准>>> 【今日歌曲推荐】 陈李雨声 : 梦想还是要有的 万一实现了呢。《secret base》 《secret base》- 茅野愛衣 / 戸松遥 / 早見沙織 手机党少年们想听歌&#xff0c;请使劲儿戳&#xff08;这里&#xff09;. 紫King : 这个大…

Assembly学习心得

http://blog.csdn.net/etmonitor/Assembly学习心得说明&#xff1a;最近开始准备把学到的.NET知识重新整理一遍&#xff0c;眼过千遍不如手过一遍&#xff0c;所以我准备记下我的学习心得&#xff0c;已备参考。J各位都是大虾了&#xff0c;如果有哪些错误或者不完整的地方&…

Oracle profile 用户资源限制 说明

一. 官网说明CREATE PROFILEhttp://download.oracle.com/docs/cd/E11882_01/server.112/e17118/statements_6010.htm#SQLRF01310Oracle recommends that you use the Database Resource Manager rather than this SQL statement to establish resource limits. The Database Re…

刚发布!2020年AI人才发展报告,这三个暗示程序员一定要知道!

最近&#xff0c;程序员届有一个重大好消息&#xff0c;可能很多人还不知道&#xff0c;那就是&#xff1a;国内某些城市已经开始程序员人才补贴了&#xff01;对于人工智能公司的项目开发、人才引进、科技研发&#xff0c;最高按照国拨经费的30%给予配套支持&#xff0c;单个项…

阿里巴巴开源技术汇总:115个软件(一)

阿里巴巴开源技术汇总&#xff1a;115个软件 摘要&#xff1a; 云栖社区近期策划了多期和开源产品相关的内容&#xff0c;如GitHub最流行的开源机器学习、大数据等项目&#xff0c;揭秘阿里Weex项目&#xff0c;Hilo开源分析等。深入挖掘&#xff0c;发现开源中国已经收集了数年…

Globalization Resources

http://blog.csdn.net/etmonitor/.NET系统学习----Globalization & Resources l 前言l 了解资源文件l 创建资源文件l 在程序中使用资源文件l 资源文件的命名和部署l 参考前言&#xff1a;在学习如何使用.NET资源文件…

用 Python 可以实现侧脸转正脸?我也要试一下!

作者 | 李秋键责编 | Carol封图 | CSDN 下载自视觉中国近几年来GAN图像生成应用越来越广泛&#xff0c;其中主要得益于GAN 在博弈下不断提高建模能力&#xff0c;最终实现以假乱真的图像生成。GAN 由两个神经网络组成&#xff0c;一个生成器和一个判别器组成&#xff0c;其中生…

Hive SQL 监控系统 - Hive Falcon

1.概述 在开发工作当中&#xff0c;提交 Hadoop 任务&#xff0c;任务的运行详情&#xff0c;这是我们所关心的&#xff0c;当业务并不复杂的时候&#xff0c;我们可以使用 Hadoop 提供的命令工具去管理 YARN 中的任务。在编写 Hive SQL 的时候&#xff0c;需要在 Hive 终端&am…

System commands can run from cmd

gpedit.msc-----组策略 sndrec32-------录音机 Nslookup-------IP地址侦测器 explorer-------打开资源管理器 logoff---------注销命令 tsshutdn-------60秒倒计时关机命令 lusrmgr.msc----本机用户和组 services.msc---本地服务设置 oobe/msoobe /a----检查XP是否激活 notepad…

做 Java 工程师,挺!好!

很多想要入行编程圈的人问到我该学哪一种语言&#xff0c;我都毫不犹豫的说Java。首先我们先看个排行榜&#xff0c;来自权威开发语言排行榜TIOBE的数据&#xff08;截止到2020年4月&#xff09;&#xff0c;可以看到Java语言依然在语言排行榜霸占第一的位置&#xff01;看到这…

使用tmpfs缓存文件提高性能

[ZT]使用tmpfs缓存文件提高性能 - 夜隼 - 博客园使用tmpfs缓存文件提高性能tmpfs是一种虚拟内存文件系统&#xff0c;它最大的特点就是它的存储空间在VM&#xff08;virtual memory&#xff09;里面。Linux系统中VM主要由RM(Real Memory)和swap组成&#xff0c;因此tmpfs最大的…

【转】RelativeLayout和LinearLayout及FrameLayout性能分析

原文&#xff1a;http://blog.csdn.net/hejjunlin/article/details/51159419 工作一段时间后&#xff0c;经常会被领导说&#xff0c;你这个进入速度太慢了&#xff0c;竞品的进入速度很快&#xff0c;你搞下优化吧&#xff1f;每当这时&#xff0c;你会怎么办&#xff1f;功能…

SQL Tips

出自&#xff1a;http://blog.csdn.net/etmonitor/一.怎样删除一个表中某个字段重复的列呀,举个例子表[table1]id name1 aa2 bb3 cc1 aa2 bb3 cc我想最后的表是这样的id name1 aa2 bb3 cc回答:将记录存到临时表#t中&#xff0c;重复的记录只存一条&#xff0c;然后将临时…

98年“后浪”科学家,首次挑战图片翻转不变性假设,一作拿下CVPR最佳论文提名​...

出品 | AI科技大本营&#xff08;ID:rgznai100&#xff09;刚刚结束的CVPR大会&#xff0c;总共收到6424篇论文中&#xff0c;仅有26篇获得最佳论文提名&#xff0c;占0.4%的比例。其中&#xff0c;康奈尔大学大四学生林之秋&#xff0c;以第一作者身份提交的“Visual Chiralit…

MySQL导入导出数据和结构

1. mysql导出数据和结构使用mysqldump命令1.1 导出全库连带数据mysqldump -u root -p app_test > app_test.sql1.2 导出指定的表&#xff0c;table1连带数据mysqldump -u root -p app_test table1 > app_test_table1.sql1.3 导出多张表&#xff0c;table1&#xff0c;tab…

图表君聊docker-仓库

图表君聊docker-仓库 今天我们来继续聊docker&#xff0c;上篇文章我们介绍了docker里的Container.今天来继续三大概念中的最后一个--仓库&#xff08;Repository)。 当我做好了一个Image&#xff0c;我该怎么和其他人分享呢&#xff1f;答案很简单&#xff0c;把他push到一个仓…

正则表达式经典教程

作者&#xff1a;ET Dreams http://blog.csdn.net/etmonitor/Regular Expressions (1) ---- What is Regular Expressions?正则表达式是常见常忘&#xff0c;所以还是记下来比较保险&#xff0c;于是就有了这篇笔记。希望对大家会有所帮助。J1&#xff0e;什么是正则表达式...…

发布了!2020年AI人才发展报告,最高补助1000万!

最近&#xff0c;程序员届有一个重大好消息&#xff0c;可能很多人还不知道&#xff0c;那就是&#xff1a;国内某些城市已经开始程序员人才补贴了&#xff01;对于人工智能公司的项目开发、人才引进、科技研发&#xff0c;最高按照国拨经费的30%给予配套支持&#xff0c;单个项…

C++资源之不完全导引(上)

发信人: NULLNULL (空空), 信区: VC标 题: C资源之不完全导引(转载)发信站: 武汉白云黄鹤站 (2005年05月05日01:42:54 星期四), 站内信件C资源之不完全导引&#xff08;完整版&#xff09;来源&#xff1a;www.csdn.net-----------------------------------------------------…