数组结构的缺点:java
1.数组的大小是固定的;算法
2.无序数组中,查找效率很低;查找O(N),插入O(1)数组
3.有序数组中,插入效率又很低;查找O(logN)使用二分法,提升查找效率,减小查找次数logN=log2(N)*3.322;插入O(N)数据结构
4.不论是哪一种数组,删除操做效率都很低。O(N)学习
本章将学习单链表、双端链表、有序链表、双向链表和有迭代器的链表。大数据
在链表上,每个数据项,都被包含在一个“链结点”中。一个链结点是某个类的对象,这个类能够叫作Link,Link类是链表类是分开的。每一个链结点对象,都包含一个对下一个链结点引用的字段(一般叫next)。可是链表自己的对象中有一个字段指向对第一个链结点的引用。this
咱们经过java代码来建立Link类:spa
public class Link { private int iData; // int类型数据 private double dData; // double类型数据 private Link next; // 下一个link对象的应用,内存地址 }
单链表插入数据的过程:设计
单链表插入数据的JAVA代码实现:code
package linkedlist; /** * @author yangjian * @date created in 11:34 2019/07/19 */ public class LinkList { private Link first; public void LinkList(){ first = null; } public boolean isEmpty(){ return (first == null); } public void insertFirst(int id, double dd){ Link newLink = new Link(id, dd); newLink.next = first; first = newLink; } public Link deleteFirst(){ Link temp = first; first = first.next; return temp; } public void displayLinkList(){ System.out.println("= displayLinkList begin :"); Link current = first; while(current != null){ current.displayLink(); current = current.next; } System.out.println("= displayLinkList end"); } } class LinkListApp{ public static void main(String [] args){ LinkList linkList = new LinkList(); linkList.insertFirst(1,10.0); linkList.insertFirst(2,20.0); linkList.insertFirst(3,30.0); linkList.insertFirst(4,40.0); linkList.insertFirst(5,50.0); linkList.displayLinkList(); } }
insertFirst()方法:
建立一个新的数据项newLink,准备插入单链表中。将新数据项的next指向表头如今的数据项first,而后将新数据项替换表头里的first。这样子表头first的数据就更新为新数据项,且新数据项的next指向了上一个first存储的数据项。
deletaFirst()方法:
获取如今的表头项first,用于当作返回结果。获取first.next数据项,插入表头,实现把链表数据的更新。
查找和删除指定链结点
find()方法:
public Link find(int key){ Link current = first; while(current.iData != key){ if(current.next == null){ return null; }else{ current = current.next; } } return current; }
从表头结点开始判断,若是表头结点不匹配,则继续从表头结点的下一个结点开始判断;直到全部结点找完判断完毕,也没有发现符合的结点;或者找到了匹配的结点为止。
delete()方法:
public Link delete(int key){ Link current = first; Link previous = first; while(current.iData != key){ if(current.next == null){ return null; }else{ previous = current; current = current.next; } } if(current == first){ first = current.next; }else{ previous.next = current.next; } return current; }
建立两个变量,一个存储当前的结点,一个存储当前结点的上一个结点;循环判断,当前结点是否知足要求,若是不知足,则获取当前结点的下一个结点继续判断,同时将当前不知足要求的结点,也存储起来,若是下一个结点命中了,则须要将命中结点的next引用,赋值给父节点对象的next,这样,命中的结点,就从链表中被移除了,而且父节点的next从指向命中结点,变动为指向命中结点的next结点,链表没有断裂。最后判断,若是要移除的结点是表头结点first,则新的表头结点为first的next。
其余方法:
好比insertAfter()方法,查找某个特定的关键结点,并在它的后面新建一个新结点。
双端链表的数据结构:
双端链表新增了一个特性:即增长了对最后一个链结点的引用。就像对表头结点的引用同样,容许直接在表尾插入一个链结点,普通的链表也能够实现这样的功能,可是须要遍历全部的结点,直到到达表尾的位置,效率很低,而双端链表,提供了表头和表尾两个链结点的引用。注意,不要把双端链表和双向链表搞混。
双端链表的java代码实现:
package linkedlist; /** * @author yangjian * @date created in 16:24 2019/07/20 */ public class FirstLastList { private Link first; private Link last; public FirstLastList() { first = null; last = null; } public boolean isEmpty() { return (first == null); } public void insertFirst(int id, double dd) { Link newLink = new Link(id, dd); if (isEmpty()) { last = newLink; } newLink.next = first; first = newLink; } public void insertLast(int id, double dd) { Link newLink = new Link(id, dd); if (isEmpty()) { first = newLink; } else { last.next = newLink; } last = newLink; } public Link deleteFirst(int id) { if (isEmpty()) { return null; } Link temp = first; if (first.next == null) { last = null; } first = first.next; return temp; } public void displayLinkList(){ System.out.println("= displayLinkList begin :"); Link current = first; while(current!= null){ current.displayLink(); current = current.next; System.out.println(""); } System.out.println("= displayLinkList end"); } }
insertFirst()方法:
须要判断链表是否为空,为空,第一次添加链结点,须要给last结点也赋值。
insertLast()方法:
须要判断链表是否为空,为空,第一次添加链结点,须要给first结点也赋值。而且要保证,每次新加入的链结点,替换存储在last引用上。
deleteFirst()方法:
须要判断,若是删除了当前 first链结点后,链表为空了,须要将last结点也赋值为null
deleteLast()方法:
双端链表在由于没有存储last链结点的父引用结点,因此再实现移除表尾结点的实现上,须要遍历整个链表,找出表尾结点的父引用结点,将父引用结点赋值到last上,效率很低。这里没有实现,后面再使用双向链表时,会讨论到这一点。
在表头插入和删除的速度很快,仅须要修改一两个引用值,随意花费时间是O(1)。
平均起来,查找、删除和在指定的链结点后面插入都须要搜索表中一半的链结点,须要O(N)次比较。在数组中执行这些操做也是O(N)次比较,可是链表仍然要快一些,由于当插入和删除结点时,链表数据不须要移动。增长的效率显著,特别是在复制时间远远大于比较时间的时候。
链表比数组的另外一一个优点是,链表不须要像数组同样指定容量,链表须要多少容量就能够扩展多少容量。数组常常由于容量太大,致使效率低下,或者容量过小,而致使内容溢出。
在上一篇文章中,介绍了栈和队列这样的数据结构,并经过数组来实现了栈和队列对数据项的操做。
如今咱们学习了链表,那么如何使用链表实现栈和队列的数据结构呢?
链表实现栈:只须要保留insertFirst()和deleteFirst()方法便可,每次插入数据项和删除数据项,都对链表的表头操做便可。
链表实现队列:只须要保留insertLast()和deleteFirst()方法便可,每次向链表的表尾插入数据项,并每次从链表的表头去除数据项便可。
数组实现栈和队列,须要维护下标;而链表只须要维护表头和表尾便可。
有序链表,就是在插入新的数据项/链结点时,根据某个关键词作排序,使链表的链结点拥有先后顺序。
有序链表的java代码实现:
package linkedlist; /** * @author yangjian * @date created in 17:46 2019/07/20 */ public class SortedList { private SortedLink first; public SortedList() { first = null; } public boolean isEmpty() { return (first == null); } public void insert(long key) { SortedLink newLink = new SortedLink(key); SortedLink current = first; SortedLink previous = null; while (current != null && current.dData > key) { previous = current; current = current.next; } if (previous == null) { first = newLink; } else { previous.next = newLink; } newLink.next = current; } public SortedLink remove() { SortedLink temp = first; first = first.next; return temp; } public void displayLinkList() { System.out.println("= displayLinkList begin :"); SortedLink current = first; while (current != null) { current.display(); current = current.next; System.out.println(""); } System.out.println("= displayLinkList end"); } } class SortedLink { public long dData; public SortedLink next; public SortedLink(long dData) { this.dData = dData; next = null; } public void display() { System.out.print("{" + dData + "}"); } } class SortedLinkApp { public static void main(String[] args) { SortedList theSortedList = new SortedList(); theSortedList.insert(10); theSortedList.insert(30); theSortedList.insert(20); theSortedList.insert(40); theSortedList.insert(50); theSortedList.displayLinkList(); theSortedList.remove(); theSortedList.displayLinkList(); } }
最重要的方法是insert()方法
有序链表的效率
有序链表插入一个数据项,最多须要O(N)次比较,平均是(N/2),跟数组同样。可是在O(1)的时间内就能够找到并删除表头的最小/最大数据项。若是一个应用频繁的存储最小项,且不须要快速的插入,那么有序链表时一个有效的方案。优先级队列就可使用有序链表来实现。
如何给一个无序数组排序?
如今有一个无序数组,若是要给无序数组进行排序,可使用数组的插入排序法,可是插入排序法的时间级为O(N的2次方)(使用了双层循环的时间级别,就是N的2次方)。这个时候,咱们能够建立一个有序链表,将无序数组的数据项,挨个取出,插入到有序链表中,由有序链表实现数据项的排序,再从有序链表取出数据项从新插回数组中,就是排序后的结果。这样作的好处是,大大减小移动次数,在数组中进行插入排序须要N的2次方移动;而是用有序链表,数据项一次从数组到链表,一次从链表到数组,相比之下2*N次移动更好。
双向链表和双端链表是不同的,由于单链表和双端链表,经过current.next能够很方便的到达下一个链结点,可是反向的遍历就很困难。双向链表提供了这个能力,即容许向后遍历,也容许向前遍历。其中的秘密就是每一个链结点,有两个指向其余链结点的引用,而不是一个。
public class Link { public long dData; public Link next; // 下一个link对象的应用,内存地址 public Link previous; // 上一个link对象的应用,内存地址 }
双端链表的意思是,链表中维护表头和表尾两个引用,由于颇有用,因此在双向链表中,也能够保留双端链表的特性。
递归的三个要素:
1.调用本身
2.每次调用本身是为了解决一个更小的问题
3.存在一个基值Base case/限制条件,当知足条件时,直接返回结果
递归中必须存在限制条件,若是没有限制条件,会形成一种算法中的庞氏骗局,永远没法结束。
三角数:1,3,6,10,15,21.....第N项等于第N-1项加N,第n个三角数字=(n的2次方+n)/2。
三角数表达递归:
int trianle(int n){ if(n==1){ return 1; }else{ int temp = n + trianle(n-1); return temp; } }
递归的效率:咱们使用递归,是由于递归从概念上简化了问题,而不是由于递归真的能够提升效率。咱们调用一个方法时,在内存上会为方法生成一个栈空间,当这个方法使用递归的时,会在栈内存中一直调用新的方法,若是这个方法的数据量很大,那么会容易引发栈内存溢出的问题。
数学概括法:
递归就是程序设计中的数学概括法。数学概括法就是一种经过自身的语汇定义某事物本身的方法。
tri(n) = 1 if n = 1
tri(n) = n + tri(n-1) if n > 1
阶乘:阶乘与三角数同样,三角数中第n项的数值等于n加上第n-1项的三角数;而阶乘中,第n项的数值等于n乘以第n-1项的阶乘,即第5项数值的阶乘等于5*4*3*2*1=120。
0的阶乘被定义为1
咱们先回顾一下基于有序数组的二分查找方法如何实现的:
package sorte; /** * @author yangjian * @date created in 18:47 2019/07/22 */ public class SortedErFenFa { private int nItems = 10; private long[] arr = new long[]{1,2,3,4,5,6,10,14,24,35}; public int find(long searchKey){ int lowIndex = 0; int upperIndex = arr.length - 1; int currentIndex; while(true){ // 每次获取比较范围的中间位置的变量下标 currentIndex = (lowIndex + upperIndex)/2; // (0 + 9)/2 = 4 // 命中 if(arr[currentIndex] == searchKey){ return currentIndex; // 若是传入的数据项,在数组中介于两个相连的元素之间,可是数组中缺不存在, // lowIndex自己是小于upperIndex的,可是随着循环次数的增长,lowIndex会等于upperIndex, // 最后lowIndex会大于currentIndex }else if(upperIndex < lowIndex){ return nItems; }else{ // 中间数大于入参,缩小范围为前半截 if(arr[currentIndex] > searchKey){ upperIndex = currentIndex - 1; // 中间数小于入参,缩小范围为后半截 }else if(arr[currentIndex] < searchKey){ lowIndex = currentIndex + 1; } } } } }
递归取代循环:上述方法还能够用递归来实现
package recursion; /** * @author yangjian * @date created in 19:12 2019/07/22 */ public class SortedErFenFa { private int nItems = 10; private long[] arr = new long[]{1,2,3,4,5,6,10,14,24,35}; public int recFind(long searchKey, int lowerIn, int upperIn){ int curIn; curIn = (lowerIn + upperIn)/2; if(arr[curIn] == searchKey){ return curIn; }else if(lowerIn > upperIn){ return nItems; }else{ if(arr[curIn] > searchKey){ return recFind(searchKey, lowerIn, curIn + 1); }else{ return recFind(searchKey, curIn - 1, upperIn); } } } }
有序数组的insert方法:
public void insert(long value) { int j; // 找到value应该插入的下标 for (j = 0; j < nItems; j++) { if (arr[j] > value) { break; } } // 给数组扩容一位,并将大于j下标的元素,向右移动 for (int k = nItems; k > j; k--) { arr[k] = arr[k - 1]; } // 将value插入到数组中 arr[j] = value; // 数组容量+1 nItems++; }
二分查找法,是分治算法的一个例子,把大问题拆分为两个更小的问题,而后对待每个小问题的解决办法也是同样的:把每一个小问题拆分为两个更小的问题,并最终解决它们。这个过程持续下去,直到达到求解的基值状况,就不用了再拆分了。
分治法一般要用到递归。一般是一个方法,含有两个对自身的调用,分别对应于问题的两个部分。在二分查找法中,也有两个递归的调用,可是只有一个是真的执行了,后面咱们遇到的归并排序,它是真正的执行了两个递归调用(对分组后的两部分数据分别排序)。
冒泡排序,插入排序和选择排序要用O(N的2次方)时间,而归并排序只要O(N*logN)。若是N是10000条数据,那么N的2次方就是100000000,而N*logN只是10000*4=40000,若是归并排序须要40秒,那么插入排序就须要将近28小时。
归并排序的一个缺点是须要在存储器中有另外一个大小等于被排序的数据项数目的数组。若是初始数组几乎占满了整个存储器,那么归并排序将不能工做,可是空间足够的话,归并排序是一个很好的选择。
归并两个有序数组
归并排序的中心是归并两个已经有序的数组A和B,就生成了数组C,数组C包含数组A和B全部的数据项,而且使它们有序的排列在C中。
非归并排序实现上图排序;
package sort; /** * @author yangjian * @date created in 09:52 2019/07/23 */ public class MergeApp { public static void main(String [] args){ int[] a = {1,4,5,7,29,45}; int[] b = {2,6,33,56}; int[] c = new int[10]; merge(a, a.length, b, b.length, c); display(c); } public static int[] merge(int[] a, int aSize, int[] b, int bSize, int[] c){ int aIn = 0; int bIn = 0; int cIn = 0; while(aIn < aSize && bIn < bSize){ if(a[aIn] <= b[bIn]){ c[cIn++] = a[aIn++]; }else{ c[cIn++] = b[bIn++]; } } while(aIn < aSize){ c[cIn++] = a[aIn++]; } while(bIn < bSize){ c[cIn++] = b[bIn++]; } return c; } public static void display(int[] c){ for(int i = 0; i < c.length; i++){ System.out.print(c[i] + " "); } } } //输出结果:1 2 4 5 6 7 29 33 45 56
经过归并排序实现排序: