Java基础数据结构和算法

来源:互联网 发布:php零基础上手怎么样 编辑:程序博客网 时间:2024/05/29 06:47

学习编程的第一天,就被告知程序=数据结构+ 算法。作为一名开发者,虽然平时单独研究数据结构和算法的情况不多,但也一直在用。这些基础知识和思想伴随着自己写的每一句代码。

数据结构

Array数组

和C/C++以及其他语言一样,Java中的数组有差不多一样的语法。只是Java中除了8中基本类型,数组也是作为对象处理的,所以创建对象时也需要使用new关键字。和大多数编程语言一样,数组一旦创建,大小便不可改变。
Java中有一个Arrays类,专门用来操作array。
Arrays中拥有一组static函数,
- equals():比较两个array是否相等。array拥有相同元素个数,且所有对应元素两两相等。
- fill():将值填入array中。
- sort():用来对array进行排序。
- binarySearch():在排好序的array中寻找元素。
- System.arraycopy():array的复制。

int [] intArr = new int[10];
  • 1
  • 1

Array是Java中随机访问一连串对象最有效率的数据结构,但很不灵活,大小固定,且不知道里面有多少元素。为此JDK已经为我们提供了一系列相应的类来实现功能强大且更灵活的基本数据结构。这些类均在java.util包中。其继承结构如下:
Collection
List
│├LinkedList
│├ArrayList
│└Vector
│ └Stack
Set
│ └SortedSet
Queue

-Map
├HashTable
├HashMap
└WeakHashMap

List

List是一个接口,不能实例化,需要实例化一个ArrayList或者LinkedList
ArrayList里面的内部实现,是通过一定的增长规则动态复制增加数组长度来实现动态增加元素的。如果在大数据量的情况下,在某一个位置随机插入或者删除元素,就会产生性能问题。LinkedList可以解决这类问题,但LinkedList在通过下标取元素的时候,需要遍历整个链表节点匹配,数据量大的情况下,效率不高。Vector是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。Stack是Java实现了一个堆栈,先进后出结构。

遍历时删除问题

使用增强for循环遍历List(所有实现子类ArrayList,Stack等)元素对其中元素进行删除,会抛出java.util.ConcurrentModificationException的异常。若使用下面这种方式:

for(int i = 0;i < list.size();i++){            list.remove(i);}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

则会删除下标为偶数的元素,因为每次删除后,后面的元素的下标全部减1,相当于元素位置全部左移一位,再次删除时,会跳过一个元素进行删除。这是非常不好的。如果非得要这样删除,可以倒着来:

for(int i = list.size()-1 ;i >= 0 ;i--){            list.remove(i);}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

或者新建一个要删除的List,最后一起删除。list.removeAll(deleteList);

Set

Set接口继承Collection接口,最大的特点是集合中的元素都是唯一的,没有重复。它有两个子类,HashSetTreeSet

HashSet

  • 不允许出现重复元素;
  • 不保证集合中元素的顺序。哈希算法来的~
  • 允许包含值为null的元素,但最多只能有一个null元素。

TreeSet

  • 不允许出现重复元素;
  • 集合中元素的顺序按某种规则进行排序
  • 不允许包含值为null的元素

Map

Map接口,没有继承Collection接口,它是独立的一个接口。它使用key-value的键值对存储数据。常用的两个子类是HashMap和TreeMap。
- HashMap:Map基于散列表的实现。插入和查询“键值对”的开销是固定的。可以通过构造器设置容量capacity和负载因子load factor,以调整容器的性能。
- LinkedHashMap: 类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插入次序,或者是最近最少使用(LRU)的次序。只比HashMap慢一点。而在迭代访问时发而更快,因为它使用链表维护内部次序。
- TreeMap : 基于红黑树数据结构的实现。查看“键”或“键值对”时,它们会被排序(次序由ComparabelComparator决定)。TreeMap的特点在 于,你得到的结果是经过排序的。TreeMap是唯一的带有subMap()方法的Map,它可以返回一个子树。
- WeakHashMao :弱键(weak key)Map,Map中使用的对象也被允许释放: 这是为解决特殊问题设计的。如果没有map之外的引用指向某个“键”,则此“键”可以被垃圾收集器回收。
- IdentifyHashMap: : 使用==代替equals()对“键”作比较的hash map。专为解决特殊问题而设计。

线程安全

Vector是线程同步的,也就是线程安全的,对多线程的操作采用了synchronized处理。但因为效率低,已不建议使用。ArrayListLinkedList都是线程不安全的,在多线程环境中,对数据的修改会造成错误的结果。有两种解决方案:

使用同步包装器

List safedList = Collections.synchronizedList(new ArrayList());Set safedSet=Collections.synchronizedSet(new HashSet());Map safedMap=Collections.synchronizedMap(new HashMap());
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

查看其源码,发现是Collections类给不安全的集合类包装了一层,然后生成一个新的类,新类里面采用了synchronized对集合的操作进行了同步处理。

...        public int size() {            synchronized (mutex) {return c.size();}        }        public boolean isEmpty() {            synchronized (mutex) {return c.isEmpty();}        }        public boolean contains(Object o) {            synchronized (mutex) {return c.contains(o);}        }        public Object[] toArray() {            synchronized (mutex) {return c.toArray();}        }        public <T> T[] toArray(T[] a) {            synchronized (mutex) {return c.toArray(a);}        }        public Iterator<E> iterator() {            return c.iterator(); // Must be manually synched by user!        }        public boolean add(E e) {            synchronized (mutex) {return c.add(e);}        }        public boolean remove(Object o) {            synchronized (mutex) {return c.remove(o);}        }        public boolean containsAll(Collection<?> coll) {            synchronized (mutex) {return c.containsAll(coll);}        }        public boolean addAll(Collection<? extends E> coll) {            synchronized (mutex) {return c.addAll(coll);}        }        public boolean removeAll(Collection<?> coll) {            synchronized (mutex) {return c.removeAll(coll);}        }        public boolean retainAll(Collection<?> coll) {            synchronized (mutex) {return c.retainAll(coll);}        }        public void clear() {            synchronized (mutex) {c.clear();}        }...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

使用安全的集合类

Java5.0新加入的ConcurrentLinkedQueueConcurrentHashMapCopyOnWriteArrayListCopyOnWriteArraySet,这些集合类都是线程安全的。这些类在 java.util.concurrent包下。
而至于这些新的类为什么能保证线程安全,这里不作详述,可以参考网上大牛的分析。

Android中的List、Map替代方案

SimpleArrayMap

SparseArray与SparseArrayCompat和LongSparseArray

这3个类中,前2个基本上是同一类,只不过第二个类有removeAt方法,第三个是Long类型的。
这3个类也是用来代替HashMap,只不过他们的键(key)的类型是整型Integer或者Long类型,在实际开发中,如月份缩写的映射,或者进行文件缓存映射,ViewHolder都特别适用

AtomicFile

AtomicFile首先不是用来代替File的,而是作为File的辅助类存在, AtomicFile的作用是实现事务性原子操作,即文件读写必须完整,适合多线程中的文件读写操作。
用来实现多线程中的文件读写的安全操作

算法

二分查找

对于有序数组,二分查找的效率在大数据量的情况下,效率明显:

private static int find(int [] arr,int searchKey){        int lowerBound = 0;        int upperBound = arr.length -1;        int curIn;        while(lowerBound <= upperBound){            curIn = (lowerBound + upperBound) / 2;            if(arr[curIn] == searchKey){                return curIn;            }else{                if(arr[curIn] < searchKey){                    lowerBound = curIn + 1;                }else{                    upperBound = curIn - 1;                }            }        }        return -1;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

使用递归的方式编写,貌似看起来好理解点:

private static int recursiveFind(int[] arr,int start,int end,int searchKey){        if (start <= end) {            // 中间位置            int middle = (start + end) >> 1; // (start+end)/2            if (searchKey == arr[middle]) {                // 等于中值直接返回                return middle;            } else if (searchKey < arr[middle]) {                // 小于中值时在中值前面找                return recursiveFind(arr, start, middle - 1, searchKey);            } else {                // 大于中值在中值后面找                return recursiveFind(arr, middle + 1, end, searchKey);            }        } else {            // 找不到            return -1;        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

排序

简单排序

冒泡排序

对乱序的数组,很常见的排序方法是冒泡排序:

private static void bubbleSrot(int[] arr) {        for (int i = 0; i < arr.length - 1; i++) {            for (int j = i + 1; j < arr.length; j++) {                if(arr[i] > arr[j]){                    int temp = arr[i];                    arr[i] = arr[j];                    arr[j] = temp;                }            }        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这种排序方法速度是很慢的,运行时间为O(N²)级。
选择排序改进了冒泡排序,将必要的交换次数从O(N²)减少到O(N),不幸的是比较次数依然是O(N²)级。
然而,选择排序依然为大记录量的排序提出了一个非常重要的改进,因为这些大量的记录需要在内存中移动,这就使交换的时间和比较的时间相比起来,交换的时间更为重要。(一般来说,Java语言中不是这种情况,Java中只是改变了引用位置,而实际对象的位置并没有发生改变)

选择排序

private static void chooseSort(int[] arr) {        for (int i = 0; i < arr.length; i++) {            int least = i;            for (int j = i + 1; j < arr.length; j++) {                if (arr[j] < arr[least]) {                    least = j;                }            }            // 将当前第一个元素与它后面序列中的最小的一个 元素交换,也就是将最小的元素放在最前端            int temp = arr[i];            arr[i] = arr[least];            arr[least] = temp;        }    }  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

选择排序的效率:选择排序和冒泡排序执行了相同次数的比较:N*(N-1)/2。对于10个数据项,需要45次比较,然而,10个数据项只需要少于10次的交换。对于100个数据项,需要4950次比较,但只进行不到100次交换。N值很大时,比较的次数是主要的,所以结论是选择排序和冒泡哦排序一样运行了O(N²)时间。但是,选择排序无疑更快,因为它进行的交换少得多。

插入排序

插入排序,在一般情况下,比冒泡排序快一倍,比选择排序快一点。

private static void insertionSort(int[] arr){        int in,out;        for(out = 1 ; out < arr.length ; out ++){            int temp = arr[out];            in = out;            while(in > 0 && arr[in-1] >= temp){                arr[in] = arr[in - 1];                --in;            }            arr[in] = temp;        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在外层的for循环中,out变量从1开始,向右移动。它标记了未排序部分的最左端数据。而在内层的while循环中,in变量从out变量开始,向左移动,直到temp变量小于in所指的数组数据项,或者它已经不能再向左移动为止。while循环的每一趟都向左移动了一个已排序的数据项。

插入排序的效率:这个算法中,第一趟排序,最多比较一次,第二趟排序,最多比较两次,以此类推,最后一趟最多比较N-1次,因此有1+2+3+…+N-1 = N*(N-1)/2。然而,因为在每一趟排序发现插入点之前,平均只有全体数据项的一半真的进行了比较,所以除以2最后是N*(N-1)/4。
对于随机顺序的数据,插入排序也需要O(N²)的时间级。当数据基本有序,插入排序几乎只需要O(N)的时间,这对把一个基本有序的文件进行排序是一个简单而有效的方法。
对于逆序排列的数据,每次比较和移动都会执行,所以插入排序不比冒泡排序快。

归并排序

归并排序比简单排序要有效的多,至少在速度上是这样的。冒泡排序、选择排序、插入排序要用O(N²)的时间,而归并排序只需要O(N*logN)的时间。
归并排序的一个缺点是它需要在存储器中有另一个大小等于被排序的数据项数目的数组。如果初始数组几乎占满整个存储器,那么归并排序将不能工作。但是,如果有足够的空间,归并排序会是一个很好的选择。
原理是合并两个已排序的数组到一个数组:

//将两个已排序的数组合并到第三个数组上。    private static void merge(int[] arrA,  int[] arrB,  int[] arrC) {        int aDex = 0, bDex = 0, cDex = 0;        int sizeA = arrA.length;        int sizeB = arrB.length;        // A数组和B数组都不为空        while (aDex < sizeA && bDex < sizeB) {            if (arrA[aDex] < arrB[bDex]) {                arrC[cDex++] = arrA[aDex++];            } else {                arrC[cDex++] = arrB[bDex++];            }        }        //A数组不为空,B数组为空        while (aDex < sizeA) {            arrC[cDex++] = arrA[aDex++];        }        //A数组为空,B数组不为空        while (bDex < sizeB) {            arrC[cDex++] = arrB[bDex++];        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

于是,详细的完整实现如下:

static class DArray{        private int [] theArray;        public DArray(int[] theArray) {            this.theArray = theArray;        }        //执行归并排序        public void mergeSort(){            //复制一份出来            int [] workSpace = new int [theArray.length];            reMergeSort(workSpace, 0, theArray.length-1);        }        private void reMergeSort(int [] workSpace,int lowerBound,int upperBound) {            if(lowerBound == upperBound){                return;            }else{                int mid = (lowerBound + upperBound) / 2;                reMergeSort(workSpace, lowerBound, mid);                reMergeSort(workSpace, mid + 1, upperBound);                merge(workSpace, lowerBound, mid + 1,upperBound);            }        }        private void merge(int [] workSpace,int lowPtr,int highPtr,int upperBound){            int j= 0;//workSpace's index            int lowerBound = lowPtr;            int mid = highPtr -1;            int n = upperBound - lowerBound + 1;            while(lowPtr <= mid && highPtr <= upperBound){                if(theArray[lowPtr] < theArray[highPtr]){                    workSpace[j++] = theArray[lowPtr++];                }else{                    workSpace[j++] = theArray[highPtr++];                }            }            while(lowPtr <= mid){                workSpace[j++] = theArray[lowPtr++];            }            while(highPtr <= upperBound){                workSpace[j++] = theArray[highPtr++];            }            for(j = 0;j < n ;j++){                theArray[lowerBound+j] = workSpace[j];            }        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

运行测试:

int b[] = new int[] { 3, 4, 1, 5, 5, 6, 8, 9, 7 };DArray dArray = new DArray(b);dArray.mergeSort();System.out.println(Arrays.toString(b));//输出结果:[1, 3, 4, 5, 5, 6, 7, 8, 9]
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

高级排序

有2个高级的排序算法,希尔排序和快速排序。这两种排序算法都比简单排序算法快得多:希尔排序大约需要O(N*(logN)²)时间,快速排序需要O(N*logN)时间。这两种排序算法都和归并排序不同,不需要大量的辅助存储空间。希尔排序几乎和归并排序一样容易实现,而快速排序是所有通用排序算法中最快的一种排序算法。
还有一种基数排序,是一种不常用但很有趣的排序算法。

希尔排序

希尔排序是基于插入排序的。

private static void shellSort(int[] arr) {        int inner, outer;        int temp;        int h = 1;        int nElem = arr.length;        while (h <= nElem / 3) {            h = h * 3 + 1;        }        while (h > 0) {            for (outer = h; outer < nElem; outer++) {                temp = arr[outer];                inner = outer;                while (inner > h - 1 && arr[inner - h] >= temp) {                    arr[inner] = arr[inner - h];                    inner -= h;                }                arr[inner] = temp;            }            h = (h - 1) / 3;        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

快速排序

快速排序是最流行的排序算法,在大多数情况下,快速排序都是最快的,执行时间是O(N*logN)级。

划分

划分是快速排序的根本机制。划分本身也是一个有用的操作。
划分数据就是把数据分为两组,使所有关键字大于特定值的数据项在一组,所有关键字小于特定值的数据项在另一组。

private static int partitionIt(int[] arr ,int left,int right,int pivot){        int leftPtr = left - 1;        int rightPtr = right + 1;        while(true){            while(leftPtr < right && arr[++leftPtr] < pivot);            while(rightPtr > 0 && arr[--rightPtr] > pivot);            if(leftPtr >= rightPtr){                break;            }else{                //交换leftPtr和rightPtr位置的元素                int temp = arr[leftPtr];                arr[leftPtr] = arr[rightPtr];                arr[rightPtr] = temp;            }        }        return leftPtr;//返回枢纽位置    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
快速排序
//快速排序    private static void recQuickSort(int arr [] ,int left,int right){        if(right - left <= 0){            return;        }else{            int pivot = arr[right];//一般使用数组最右边的元素作为枢纽            int partition = partitionIt(arr, left, right, pivot);            recQuickSort(arr, left, partition-1);            recQuickSort(arr, partition+1, right);        }    }    //划分    private static int partitionIt(int[] arr ,int left,int right,int pivot){        int leftPtr = left - 1;        //int rightPtr = right + 1;        int rightPtr = right ; //使用最右边的元素作为枢纽,划分时就要将最右端的数据项排除在外        while(true){            while(arr[++leftPtr] < pivot);            while(rightPtr > 0 && arr[--rightPtr] > pivot);            if(leftPtr >= rightPtr){                break;            }else{                //交换leftPtr和rightPtr位置的元素                int temp = arr[leftPtr];                arr[leftPtr] = arr[rightPtr];                arr[rightPtr] = temp;            }        }        //交换leftPtr和right位置的元素        int temp = arr[leftPtr];        arr[leftPtr] = arr[right];        arr[right] = temp;        return leftPtr;//返回枢纽位置    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

最后测试,10万条随机数据,排序完成耗时18~25ms。希尔排序耗时差不多,而简单排序中的插入排序和选择排序耗时3500ms以上,冒泡排序最慢,超过17000ms以上才完成;归并排序比希尔排序和快速排序稍微慢点,在30ms左右。

文章是查找了很多的文章,还有Java数据结构和算法.(第二版)这本书总结出来的。作为一个Java开发者,这些基础的知识必须掌握,并且对其原理和源码要有所理解和领悟。感谢网上的大牛和无私的小伙伴们的分享。欢迎纠正和探讨。

(function () {('pre.prettyprint code').each(function () { var lines = (this).text().split(\n).length;varnumbering = $('
    ').addClass('pre-numbering').hide(); (this).addClass(hasnumbering).parent().append(numbering); for (i = 1; i
    0 0