链表
数组结构的缺点:
1.数组的大小是固定的;
2.无序数组中,查找效率很低;查找O(N),插入O(1)
3.有序数组中,插入效率又很低;查找O(logN)使用二分法,提高查找效率,减少查找次数logN=log2(N)*3.322;插入O(N)
4.不管是哪种数组,删除操作效率都很低。O(N)
本章将学习单链表、双端链表、有序链表、双向链表和有迭代器的链表。
链结点(Link)
在链表上,每一个数据项,都被包含在一个“链结点”中。一个链结点是某个类的对象,这个类可以叫做Link,Link类是链表类是分开的。每个链结点对象,都包含一个对下一个链结点引用的字段(通常叫next)。但是链表本身的对象中有一个字段指向对第一个链结点的引用。
我们通过java代码来创建Link类:
public class Link {
private int iData; // int类型数据
private double dData; // double类型数据
private Link next; // 下一个link对象的应用,内存地址
}
单链表
单链表插入数据的过程:
单链表插入数据的JAVA代码实现:
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
通过归并排序实现排序: