转载请标明出处,谢谢!
https://www.jianshu.com/p/3aeb5998e79e
关联文章
冒泡、选择排序 https://www.jianshu.com/p/176b0b892591
栈和队列 https://www.jianshu.com/p/8cb602ef4e21
顺序表、单双链表 https://www.jianshu.com/p/3aeb5998e79e
二叉树 https://www.jianshu.com/p/de829eab944c
图论 https://www.jianshu.com/p/cf03e51a3ca2
顺序表
在顺序表中,最常见的操作当然是查找(搜索),插入和删除了,现在对这三种操作的复杂度进行简要的分析。
第一,搜索:
顺序表的顺序搜索算法,它的主要内容就是从顺序表的第一项开始,根据要查找的给定值x,然后在顺序表中逐次进行比较。若相等,则搜索成功,返回它的所在位置。若遍历整个表,没有值与其相等,则返回搜索失败。
一般来说,搜索算法的复杂度是根据比较次数来决定的。简要分析,如果我们要找的值在第一个表项,那么它的比较次数就是1,当然要是在第二个表项,次数就为2,依次类推,在第n个表项,比较次数就为n。好了啦,现在我们可以计算它的平均比较次数了。假定每个表项的可能性都相等,那么我们有:
Acn=1/nΣi=1/n(1+2+.....+n)=1/nn(n+1)/2=(n+1)/2;
所以平均比较次数为(n+1)/2;
第二,插入:
顺序表的插入和删除算法复杂度与其表项循环内数据移动次数直接有关,先分析插入。我们知道,顺序表中如果要在某个位置插入一个元素,就必须把那个空出来,怎么空出来呢,其实就是把它以及它后面的元素向后移动一位。那么就是这样的,如果将新表项插入至第i个表项后面,可以从后向前循环,逐个向后移动n-i个表项。最好的情形就是在表尾追加新元素。那么它的移动次数为0,相反,最坏情形是在第一个表项位置插入。那么移动次数为n。来看平均移动次数。
Amn=1/(n+1)Σ(n-i)=1/(n+1)((n-1)+...+1+0)=(n)/2;
第三,删除:
在删除第i个表项时,是逐个移动n-i个表项。最好的情况是在删去最后的n个表项。次数为0,最坏情况是删除第一个表项。移动个数为n-1。那么平均移动个数:
Amn=1/nΣ(n-i)=1/n((n-1)+...+1+0)=(n-1)/2;
原文:https://blog.csdn.net/lifestylegoingon/article/details/47903743
总结:
通过上述的分析,我们对顺序表的实现已有了比较清晰的认识,接下来看一下顺序表的执行效率问题,主要针对获取、插入、修改、删除等主要操作。前面分析过,由于顺序表内部采用了数组作为存储容器,而数组又是随机存取结构的容器,也就是说在创建数组时操作系统给数组分配的是一块连续的内存空间,数组中每个存储单元的地址都是连续的,所以在知道数组基地址后可以通过一个简单的乘法和加法运算即可计算出其他存储单元的内存地址(实际上计算机内部也就是这么做的),这两个运算的执行时间是常数时间,因此可以认为数组的访问操作能在常数时间内完成,即顺序表的访问操作(获取和修改元素值)的时间复杂为O(1)。
对于在顺序表中插入或者删除元素,从效率上则显得不太理想了,由于插入或者删除操作是基于位置的,需要移动数组中的其他元素,所以顺序表的插入或删除操作,算法所花费的时间主要是用于移动元素,如在顺序表头部插入或删除时,效率就显得相当糟糕了。
优点
使用数组作为内部容器简单且易用
在访问元素方面效率高
数组具有内存空间局部性的特点,由于本身定义为连续的内存块,所以任何元素与其相邻的元素在物理地址上也是相邻的。
缺点
内部数组大小是静态的,在使用前必须指定大小,如果遇到容量不足时,需动态拓展内部数组的大小,会造成额外的时间和空间开销
在内部创建数组时提供的是一块连续的空间块,当规模较大时可能会无法分配数组所需要的内存空间
顺序表的插入和删除是基于位置的操作,如果需要在数组中的指定位置插入或者删除元素,可能需要移动内部数组中的其他元素,这样会造成较大的时间开销,时间复杂度为O(n)
链表
通过前面对线性顺序表的分析,我们知道当创建顺序表时必须分配一块连续的内存存储空间,而当顺序表内部数组的容量不足时,则必须创建一个新的数组,然后把原数组的的元素复制到新的数组中,这将浪费大量的时间。而在插入或删除元素时,可能需要移动数组中的元素,这也将消耗一定的时间。鉴于这种种原因,于是链表就出场了,链表在初始化时仅需要分配一个元素的存储空间,并且插入和删除新的元素也相当便捷,同时链表在内存分配上可以是不连续的内存,也不需要做任何内存复制和重新分配的操作。
public class Node<T> {
//链表数据域
T t;
//链表指针域
Node<T> next;
public Node(T t) {
this.t = t;
}
}
public Node<T> head;
public int size;
}
单链表添加:链表添加分为头部添加和其他
/**
* 单链表添加数据
*
* @param index
* @param data
*/
public void add(int index, T data) {
if (data == null) {
return;
}
if (index < 0 || index > size) {
System.out.println("插入位置不对");
return;
}
//在头部插入
if (head == null ) {
head = new Node(data);
} else {
Node front = head;
//找到要插入结点位置的前一个结点
for (int i = 0; i < index - 1; i++) {
if (front.next != null) {
front = front.next;
}
}
Node newNode = new Node(data);
newNode.next = front.next;
front.next = newNode;
}
size++;
}
添加头部位置:头部结点为空所以直接将新结点赋值给头部位置
添加中间(尾部)位置:先找到要添加结点的前一个位置,新结点的next=front.next, front.next指向新结点的位置。记得判空,因为尾部的next是空
单链表删除:链表删除分为头部删除和其他
删除头部位置:删除头部位置,及它的next变成新的head.
删除中间位置:先找到要删除结点的前一个位置,该位置的next直接指向该位置next的next.
/**
* 删除结点
*
* @param index
*/
public void delNode(int index) {
if (index < 0 || index > size) {
System.out.println("删除位置不对");
return;
}
/*删除头结点*/
if (index == 0 && head != null) {
head = head.next;
} else {
Node front = head;
//找到要删除结点位置的前一个结点
for (int i = 0; i < index - 1; i++) {
if (front.next != null) {
front = front.next;
}
}
if (front.next != null) {
front.next = front.next.next;
}
}
size--;
}
其他方法:
/**
* 判断是否是空
* @return
*/
public boolean isEmpty() {
return head == null;
}
/**
* 清空结点
*/
public void clearLinked() {
if (!isEmpty()) {
head = null;
}
}
/**
* 显示出所有的节点信息
*/
public void displayAllNode() {
Node current = head;
while (current != null) {
System.out.print(current.t + " ");
current = current.next;
}
}
测试:
@Test
public void test() {
SignLinkedList signLinkedList = new SignLinkedList();
signLinkedList.add(0, "a");
signLinkedList.add(1, "b");
signLinkedList.add(2, "c");
signLinkedList.add(3, "d");
signLinkedList.add(4, "e");
signLinkedList.displayAllNode();
System.out.println("\n----删除头结点----");
signLinkedList.delNode(0);
signLinkedList.displayAllNode();
System.out.println("\n----删除中间结点----");
signLinkedList.delNode(2);
signLinkedList.displayAllNode();
System.out.println("\n----删除尾结点----");
signLinkedList.delNode(2);
signLinkedList.displayAllNode();
System.out.println("\n----清空数据----");
signLinkedList.clearLinked();
signLinkedList.displayAllNode();
}
单链表性能分析:
由于单链表并不是随机存取结构,即使单链表在访问第一个结点时花费的时间为常数时间,但是如果需要访问第i(0<i<n)个结点,需要从头结点head开始遍历部分链表,进行i次的p=p.next操作,这点从上述的图文分析我们也可以看出,这种情况类似于前面计算顺序表需要平均移动元素的总数,因此链表也需要平均进行n2n2次的p=p.next操作,也就是说get(i)和set(i,x)的时间复杂度都为O(n)。
由于链表在插入和删除结点方面十分高效的,因此链表比较适合那些插入删除频繁的场景使用,单纯从插入操作来看,我们假设front指向的是单链表中的一个结点,此时插入front的后继结点所消耗的时间为常数时间O(1),但如果此时需要在front的前面插入一个结点或者删除结点自己时,由于front并没有前驱指针,单凭front根本无法知道前驱结点,所以必须从链表的表头遍历至front的前一个结点再执行插入或者删除操作,而这个查询操作所消耗的时间为O(n),因此在已知front结点需要插入前驱结点或者删除结点自己时,消耗的时间为O(n)。当然这种情况并不是无法解决的,后面我们要分析到的双链表就可以很好解决这个问题,双链表是每个结点都同时拥有前后继结点的链表,这样的话上面的问题就迎刃而解了。上述是从已知单链表中front结点的情况下讨论的单链表的插入删除效率。
我们可能会有个疑问,从前面单链表的插入删除的代码实现上来说,我们并不知道front结点的,每次插入和删除结点,都需要从表头开始遍历至要插入或者删除结点的前一个结点,而这个过程所花费的时间和访问结点所花费的时间是一样的,即O(n),
也就是说从实现上来说确实单链表的插入删除操作花费时间也是O(n),而顺序表插入和删除的时间也是O(n),那为什么说单链表的插入和删除的效率高呢?这里我们要明白的是链表的插入和删除之所以是O(N),是因为查询插入点所消耗的,找到插入点后插入操作消耗时间只为O(1),而顺序表查找插入点的时间为O(1),但要把后面的元素全部后移一位,消耗时间为O(n)。问题是大部分情况下查找所需时间比移动短多了,还有就是链表不需要连续空间也不需要扩容操作,因此即使时间复杂度都是O(n),所以相对来说链表更适合插入删除操作。
原文:https://blog.csdn.net/javazejian/article/details/52953190
单链表的反转
https://mp.weixin.qq.com/s?src=11×tamp=1590225130&ver=2356&signature=ir7GLbKq7oqI0usHtgvIBItkfEZScqO0XH3inr2F2in9Bb36xsWkOblF8An6XqTLG3ff0jyoHPUzLGqQZVw0RAvcHU-NYudxl3lNQX-kPLpI9fD0uxFx1nLlb4g395&new=1
双链表
双链表的主要优点是对于任意给的结点,都可以很轻易的获取其前驱结点或者后继结点,而主要缺点是每个结点需要添加额外的next域,因此需要更多的空间开销,同时结点的插入与删除操作也将更加耗时,因为需要更多的指针指向操作。
在插入双链表时需分两种情况,一种是在插入空双链表和尾部插入,另一种是双链表的中间插入,如下图在空双链表插入值x:
原文:https://blog.csdn.net/javazejian/article/details/52953190
代码:
public class Node<T> {
public Node pre;
public Node next;
public T data;
public Node(Node pre, Node next, T data) {
this.pre = pre;
this.next = next;
this.data = data;
}
}
尾部的插入:如果是空表则将新节点设置位first结点,反之添加在尾部位置。
/**
* 从尾部添加
* @param data
*/
public void addLast(T data) {
Node newNode = new Node(last, null, data);
Node l = last;
last = newNode;
if (l == null) {
first = newNode;
} else {
l.next = newNode;
}
size++;
}
指定位置添加: 在指定位置添加需要将新结点的前驱指向front后继指向front的next。故新结点为:
Node newNode = new Node(front, front.next, data);
接着更改front的next指向newNode,front的next的pre指向newNode。这样原本A、B两个的连接点就断开,重新指向新的位置。
代码表示:
/**
* 指定位置添加
* @param index
* @param data
*/
public void add(int index, T data) {
if (data == null) {
return;
}
if (index < 0 || index > size) {
System.out.println("插入位置不对");
return;
}
/*如果指定的位置等于在表的尾部*/
if (index == size) {
addLast(data);
} else {
Node front = first;
/*先将指针移向将要出入位置的前一个位置*/
for (int i = 0; i < index - 1; i++) {
if (front.next != null) {
front = front.next;
}
}
/*创建将要插入的结点 新结点前驱指向front,后继指向front的后继*/
Node newNode = new Node(front, front.next, data);
if (front.next != null) {
front.next.pre = newNode;
//更改front的后继指针
front.next = newNode;
}
size++;
}
}
根据index获取结点
/**
* 根据index获取结点
* @param index
* @return
*/
private Node<T> getNode(int index) {
Node node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
删除结点:删除结点包括删除尾结点,头结点以及中间结点。
/**
* 删除结点
* @param index
*/
public void removeNode(int index){
if(index<0||index>size){
System.out.println("删除位置不对");
return;
}
/*根据指定位置获取当前要删除的结点*/
Node delNode = getNode(index);
/*删除头结点*/
if(delNode.pre == null){
first = delNode.next ;
}
/*刪除尾结点*/
if(delNode.next == null){
last = delNode.pre;
}
/*删除中间结点*/
if(delNode.pre !=null && delNode.next!=null){
delNode.pre.next = delNode.next ;
delNode.next.pre = delNode.pre;
}
size-- ;
}
测试:
@Test
public void test() {
LinkedList linkedList = new LinkedList();
linkedList.add(0, "1");
linkedList.add(1, "2");
linkedList.add(2, "3");
linkedList.add(3, "4");
linkedList.add(3,"5");
linkedList.add("last");
for (int i = 0; i < linkedList.size; i++) {
System.out.println("位置 " + linkedList.getData(i));
}
linkedList.removeNode(2);
linkedList.removeNode(linkedList.size-1);
System.out.println("删除数据后");
for (int i = 0; i < linkedList.size; i++) {
System.out.println("位置 " + linkedList.getData(i));
}
}
总结
顺序表和链表存储的优缺点
1.顺序表存储
原理:顺序表存储是将数据元素放到一块连续的内存存储空间,存取效率高,速度快。但是不可以动态增加长度
优点:存取速度高效,通过下标来直接存储
缺点:1.插入和删除比较慢,2.不可以增长长度
比如:插入或者删除一个元素时,整个表需要遍历移动元素来重新排一次顺序
2.链表存储
原理:链表存储是在程序运行过程中动态的分配空间,只要存储器还有空间,就不会发生存储溢出问题
优点:插入和删除速度快,保留原有的物理顺序,比如:插入或者删除一个元素时,只需要改变指针指向即可
缺点:查找速度慢,因为查找时,需要循环链表访问
从它们的存储优缺点来看,各自有各自的使用场景,比如:频繁的查找却很少的插入和删除操作可以用顺序表存储,如果频繁的插入和删除操作很少的查询就可以使用链表存储
原文链接:https://blog.csdn.net/u013538542/article/details/47336179