Java数据结构和算法(七)——链表
前面博客我们在讲解数组中,知道数组作为数据存储结构有一定的缺陷。在无序数组中,搜索性能差,在有序数组中,插入效率又很低,而且这两种数组的删除效率都很低,并且数组在创建后,其大小是固定了,设置的过大会造成内存的浪费,过小又不能满足数据量的存储。
1、链表(Linked List)
链表通常由一连串节点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的链接(“links”)
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
2、单向链表(Single-Linked List)
单链表是链表中结构最简单的。一个单链表的节点(Node)分为两个部分,第一个部分(data)保存或者显示关于节点的信息,另一个部分存储下一个节点的地址。最后一个节点存储地址的部分指向空值。
单向链表只可向一个方向遍历,一般查找一个节点的时候需要从第一个节点开始每次访问下一个节点,一直访问到需要的位置。而插入一个节点,对于单向链表,我们只提供在链表头插入,只需要将当前插入的节点设置为头节点,next指向原头节点即可。删除一个节点,我们将该节点的上一个节点的next指向该节点的下一个节点。
/链表的每个节点类
private class Node{
private Object data;//每个节点的数据 15 private Node next;//每个节点指向下一个节点的连接 16
public Node(Object data){
this.data = data;
}
}
//在链表头添加元素 23 public Object addHead(Object obj){
Node newHead = new Node(obj);
if(size == 0){
head = newHead;
}else{
newHead.next = head;
head = newHead;
}
size++;
return obj;
}
4、双端链表
对于单项链表,我们如果想在尾部添加一个节点,那么必须从头部一直遍历到尾部,找到尾节点,然后在尾节点后面插入一个节点。这样操作很麻烦,如果我们在设计链表的时候多个对尾节点的引用,那么会简单很多。
//链表尾新增节点 38 public void addTail(Object data){
Node node = new Node(data);
if(size == 0){//如果链表为空,那么头节点和尾节点都是该新增节点 41 head = node;
tail = node;
size++;
}else{
tail.next = node;
tail = node;
size++;
}
}
6、有序链表
在有序链表中,数据是按照关键值有序排列的。一般在大多数需要使用有序数组的场合也可以使用有序链表。有序链表优于有序数组的地方是插入的速度(因为元素不需要移动),另外链表可以扩展到全部有效的使用内存,而数组只能局限于一个固定的大小中。
//插入节点,并按照从小到大的顺序排列
public void insert(int value){
Node node = new Node(value);
Node pre = null;
Node current = head;
while(current != null && value > current.data){
pre = current;
current = current.next;
}
if(pre == null){
head = node;
head.next = current;
}else{
pre.next = node;
node.next = current;
}
7、有序链表和无序数组组合排序
比如有一个无序数组需要排序,前面我们在讲解冒泡排序、选择排序、插入排序这三种简单的排序时,需要的时间级别都是O(N2)。
现在我们讲解了有序链表之后,对于一个无序数组,我们先将数组元素取出,一个一个的插入到有序链表中,然后将他们从有序链表中一个一个删除,重新放入数组,那么数组就会排好序了。和插入排序一样,如果插入了N个新数据,那么进行大概N2/4次比较。但是相对于插入排序,每个元素只进行了两次排序,一次从数组到链表,一次从链表到数组,大概需要2*N次移动,而插入排序则需要N2次移动,
效率肯定是比前面讲的简单排序要高,但是缺点就是需要开辟差不多两倍的空间,而且数组和链表必须在内存中同时存在,如果有现成的链表可以用,那么这种方法还是挺好的。
8、双向链表
我们知道单向链表只能从一个方向遍历,那么双向链表它可以从两个方向遍历。 可以用双向链表来实现双端队列
Java数据结构和算法(八)——递归
一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。
1、递归的定义
递归,就是在运行的过程中调用自己。
递归必须要有三个要素:
①、边界条件
②、递归前进段
③、递归返回段
当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
2、求一个数的阶乘:n!
规定:
①、0!=1
②、1!=1
③、负数没有阶乘
上面的表达式我们先用for循环改写
public static int getFactorial(int n){
if(n >= 0){
if(n==0){
System.out.println(n+"!=1");
return 1;
}else{
System.out.println(n);
int temp = n*getFactorial(n-1);
System.out.println(n+"!="+temp);
return temp;
}
}
return -1;
}
3、递归的二分查找
注意:二分查找的数组一定是有序的!!!将
在有序数组array[]中,不断将数组的中间值(mid)和被查找的值比较,如果被查找的值等于array[mid],就返回下标mid; 否则,就查找范围缩小一半。如果被查找的值小于array[mid], 就继续在左半边查找;如果被查找的值大于array[mid], 就继续在右半边查找。 直到查找到该值或者查找范围为空时, 查找结束。
二分查找用递归来改写,相信也很简单。边界条件是找到当前值,或者查找范围为空。否则每一次查找都将范围缩小一半。
public static int search(int[] array,int key,int low,int high)
{
int mid = (high-low)/2+low;
if(key == array[mid]){//查找值等于当前值,返回数组下标
return mid;
}
else if(low > high){//找不到查找值,返回-1
return -1;
}
else{
if(key < array[mid]){//查找值比当前值小
return search(array,key,low,mid-1);
}
if(key > array[mid]){//查找值比当前值大
return search(array,key,mid+1,high);
}
}
return -1;
}
递归的二分查找和非递归的二分查找效率都为O(logN),递归的二分查找更加简洁,便于理解,但是速度会比非递归的慢。
4、分治算法
当我们求解某些问题时,由于这些问题要处理的数据相当多,或求解过程相当复杂,使得直接求解法在时间上相当长,或者根本无法直接求出。对于这类问题,我们往往先把它分解成几个子问题,找到求出这几个子问题的解法后,再找到合适的方法,把它们组合成求整个问题的解法。如果这些子问题还较大,难以解决,可以再把它们分成几个更小的子问题,以此类推,直至可以直接求出解为止。这就是分治策略的基本思想。
上面讲的递归的二分查找法就是一个分治算法的典型例子,分治算法常常是一个方法,在这个方法中含有两个对自身的递归调用,分别对应于问题的两个部分。
二分查找中,将查找范围分成比查找值大的一部分和比查找值小的一部分,每次递归调用只会有一个部分执行。
5、归并排序
归并算法的中心是归并两个已经有序的数组。归并两个有序数组A和B,就生成了第三个有序数组C。数组C包含数组A和B的所有数据项。
public static int[] sort(int[] a,int[] b){
int[] c = new int[a.length+b.length];
int aNum = 0,bNum = 0,cNum=0;
while(aNum<a.length && bNum < b.length){
if(a[aNum] >= b[bNum]){//比较a数组和b数组的元素,谁更小将谁赋值到c数组
c[cNum++] = b[bNum++];
}else{
c[cNum++] = a[aNum++];
}
}
//如果a数组全部赋值到c数组了,但是b数组还有元素,则将b数组剩余元素按顺序全部复制到c数组
while(aNum == a.length && bNum < b.length){
c[cNum++] = b[bNum++];
}
//如果b数组全部赋值到c数组了,但是a数组还有元素,则将a数组剩余元素按顺序全部复制到c数组
while(bNum == b.length && aNum < a.length){
c[cNum++] = a[aNum++];
}
return c;
}
6、消除递归
递归和栈
递归和栈有这紧密的联系,而且大多数编译器都是用栈来实现递归的,当调用一个方法时,编译器会把这个方法的所有参数和返回地址都压入栈中,然后把控制转移给这个方法。当这个方法返回时,这些值退栈。参数消失了,并且控制权重新回到返回地址处。
调用一个方法时所发生的事:
一、当一个方法被调用时,它的参数和返回地址被压入一个栈中;
二、这个方法可以通过获取栈顶元素的值来访问它的参数;
三、当这个方法要返回时,它查看栈以获得返回地址,然后这个地址以及方法的所有参数退栈,并且销毁。
Java数据结构和算法(九)——高级排序
1、希尔排序
希尔排序应运而生了,希尔排序通过加大插入排序中元素的间隔,并在这些有间隔的元素中进行插入排序,从而使数据项能够大跨度的移动。当这些数据项排过一趟序后,希尔排序算法减小数据项的间隔再进行排序,依次进行下去,最后间隔为1时,就是我们上面说的简单的直接插入排序。
但是无论是什么间隔序列,最后必须满足一个条件,就是逐渐减小的间隔最后一定要等于1,因此最后一趟排序一定是简单的插入排序。
//希尔排序 间隔序列2h
public static void shellSort(int[] array){
System.out.println("原数组为"+Arrays.toString(array));
int step;
int len = array.length;
for(step = len/2 ;step > 0 ; step /= 2){
//分别对每个增量间隔进行排序
for(int i = step ; i < array.length ; i++){
int j = i;
int temp = array[j];
if(array[j] < array[j-step]){
while(j-step >=0 && temp < array[j-step]){
array[j] = array[j-step];
j -= step;
}
array[j] = temp;
}
}
System.out.println("间隔为"+step+"的排序结果为"+Arrays.toString(array));
}
}
2、快速排序
快速排序是对冒泡排序的一种改进,由C. A. R. Hoare在1962年提出的一种划分交换排序,采用的是分治策略(一般与递归结合使用),以减少排序过程中的比较次数。
①、快速排序的基本思路
一、先通过第一趟排序,将数组原地划分为两部分,其中一部分的所有数据都小于另一部分的所有数据。原数组被划分为2份
二、通过递归的处理, 再对原数组分割的两部分分别划分为两部分,同样是使得其中一部分的所有数据都小于另一部分的所有数据。 这个时候原数组被划分为了4份
三、就1,2被划分后的最小单元子数组来看,它们仍然是无序的,但是! 它们所组成的原数组却逐渐向有序的方向前进。
四、这样不断划分到最后,数组就被划分为多个由一个元素或多个相同元素组成的单元,这样数组就有序了。
划分的过程涉及到三个关键字:“基准元素”、“左游标”、“右游标”
基准元素:它是将数组划分为两个子数组的过程中,用于界定大小的值,以它为判断标准,将小于它的数组元素“划分”到一个“小数值的数组”中,而将大于它的数组元素“划分”到一个“大数值的数组”中,这样,我们就将数组分割为两个子数组,而其中一个子数组的元素恒小于另一个子数组里的元素。
左游标:它一开始指向待分割数组最左侧的数组元素,在排序的过程中,它将向右移动。
右游标:它一开始指向待分割数组最右侧的数组元素,在排序的过程中,它将向左移动。
注意:上面描述的基准元素/右游标/左游标都是针对单趟排序过程的, 也就是说,在整体排序过程的多趟排序中,各趟排序取得的基准元素/右游标/左游标一般都是不同的。
对于基准元素的选取,原则上是任意的。但是一般我们选取数组中第一个元素为基准元素(假设数组是随机分布的)
public class QuickSort {
//数组array中下标为i和j位置的元素进行交换
private static void swap(int[] array , int i , int j){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
private static void recQuickSort(int[] array,int left,int right){
if(right <= left){
return;//终止递归
}else{
int partition = partitionIt(array,left,right);
recQuickSort(array,left,partition-1);// 对上一轮排序(切分)时,基准元素左边的子数组进行递归
recQuickSort(array,partition+1,right);// 对上一轮排序(切分)时,基准元素右边的子数组进行递归
}
}
private static int partitionIt(int[] array,int left,int right){
//为什么 j加一个1,而i没有加1,是因为下面的循环判断是从--j和++i开始的.
//而基准元素选的array[left],即第一个元素,所以左游标从第二个元素开始比较
int i = left;
int j = right+1;
int pivot = array[left];// pivot 为选取的基准元素(头元素)
while(true){
while(i<right && array[++i] < pivot){}
while(j > 0 && array[--j] > pivot){}
if(i >= j){// 左右游标相遇时候停止, 所以跳出外部while循环
break;
}else{
swap(array, i, j);// 左右游标未相遇时停止, 交换各自所指元素,循环继续
}
}
swap(array, left, j);//基准元素和游标相遇时所指元素交换,为最后一次交换
return j;// 一趟排序完成, 返回基准元素位置(注意这里基准元素已经交换位置了)
}
public static void sort(int[] array){
recQuickSort(array, 0, array.length-1);
}
//测试
public static void main(String[] args) {
//int[] array = {7,3,5,2,9,8,6,1,4,7};
int[] array = {9,9,8,7,6,5,4,3,2,1};
sort(array);
for(int i : array){
System.out.print(i+" ");
}
//打印结果为:1 2 3 4 5 6 7 7 8 9
}
}
对快速排序来说,拥有两个大小相等的子数组是最优的情况
部分内容借鉴于:https://www.cnblogs.com/ysocean/ 如有异议,麻烦留言