线性数据结构:从物理存储上分为顺序结构、链式结构。具体包含数组、链表、栈、队列四种线性结构。
一、数组:顺序存储的线性结构,它占用连续内存空间,存储相同数据类型的数据。
1、特点:
利于查找(直接通过下标查询);不利于插入、删除,其时间复杂度为O(n)。
数组的定义:
int a[] = new int[5];//初始化时必须定义空间大小
int a[] = new int[] {1,2,3};//直接在定义时赋值
数组的插入操作:【在数组a的第i个位置插入元素x】
for(int j = a.length ; j > i ;j--){//元素集体后移
a[j+1] = a[j];
}
a[i] = x;//在i位置插入该元素
a.length + 1;//数组长度加一
数组的删除操作:【删除数组a的第i个元素】
for(int j = i+1 ; j < a.length ; j++){ //元素集体前移
a[j-1] = a[j];
}
a.length - 1;//数组长度减一
2、针对数组类型,Java提供了容器类ArrayList,其底层采用数组实现,封装了对数组的增、删、改、查操作细节。此外ArrayList最大优势是支持动态扩容。每次当存储空间不够时,ArrayList自动扩容1.5倍,默认可以存10个元素,但是这会涉及到内存的申请和数据的迁移。
ArrayList操作:
初始化 List<String> list = new ArrayList();
【泛型不能是基本数据类型,可以是它们的包装类】
添加元素 add(E e)、add(int index,E e)
删除元素 remove(int index)、
获取元素 get(int index)
得到数组长度 size()
修改元素 set(int index,E e)
获得元素在数组中首次出现的位置 indexOf(E e)
获得元素在数组中最后一次出现的位置 lastIndexOf(E e)
ArrayList源码分析:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}
transient Object[] elementData; // 存放数组元素
private int size;//数组容量大小
//添加元素:
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//数组添加第一个元素时,minCapacity = 10
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) { //扩容方法
int oldCapacity = elementData.length;//当前数组容量(旧容量)
int newCapacity = oldCapacity + (oldCapacity >> 1);//新容量 = 旧容量 * 1.5
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//将原数组复制进扩容后的数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
二、链表:链式存储的线性结构,占用一组离散的内存空间,存储相同数据类型的数据。链表由若干个结点组成,每个结点有数据域和指针域两部分。
1、特点:易于插入、删除;不利于查询,其时间复杂度为O(n)。
2、链表的分类:单链表、循环单链表、双链表、循环双链表。
单链表的定义:
public class Node {
int data;//数据域
Node next;//指针域,指向后继结点
public Node(int data) {
this.data = data;
}
}
单链表的插入
/**
* 尾插法插入链表:遍历整个链表找到最后一个节点再插入
*/
public void insertNode(int data) {
Node newNode = new Node(data);
if (head == null) {// 头结点head为空
head = newNode;
return;
}
Node node = head;
while (node.next != null) { // 遍历链表找到尾结点
node = node.next;
}
node.next = newNode;// 在尾结点后插入
}
/**
* 头插法插入链表:将新节点插入到head前面,并将新节点令为head
*/
public void insertHead(int data) {
Node newNode = new Node(data);
if (head == null) {
head = newNode;
return;
}
newNode.next = head;//插入结点指向头结点
head = newNode;//插入结点赋值为头结点
}
/**
*在链表中间插入结点:如结点preNode后面插入newNode
*/
newNode.next =preNode.next ;
preNode.next = newNode;
单链表的删除
/**
* 删除第index个节点:删除结点必须知道要删除结点的前驱
*/
public boolean deleteNode(int index) {
if (index == 1) {// 单链表只有一个结点
head = head.next;
return true;
}
Node preNode = head;
Node curNode = head.next;
int i = 1;
while (curNode != null) {
i++;
if (index == i) {
preNode.next = curNode.next;
return true;
}
preNode = preNode.next;
curNode = curNode.next;
}
return true;
}
单链表的遍历:
/**
* 展示链表:不断查找当前的结点的后继
*/
public void display() {
if (head == null) {
System.out.println("当前链表无节点,请插入");
return;
}
Node node = head;//从头结点开始
while (node != null) {
System.out.print(node.data + " ");
node = node.next;
}
}
3、针对链表类型,Java提供LinkedList,封装了对链表各种操作细节。
LinkedList操作
初始化:List<String> list = new LinkedList();
【泛型不能是基本数据类型,可以是它们的包装类】
【LinkedList是双链表,在源码分析中可以看见!!!】
添加 addFirst(E e)、addLast(E e)、add(E e)、add(int index,E e)
删除 remove(E e)、removeFirst()、removeLast()、remove(int index)
获取元素 getFirst()、getLast()、get(int index)
长度 size()
LinkedList源码分析:
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
transient Node<E> first;//头结点
transient Node<E> last;//尾结点
private static class Node<E> {//静态内部类:定义结点
E item;//数据域
Node<E> next;//后继
Node<E> prev;//前驱
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
//尾插法插入数据:
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;//尾结点给l
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;//插入结点为新的尾结点
if (l == null)//链表为空
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
//获取元素:
public E get(int index) {
checkElementIndex(index);//检查索引是否合理
return node(index).item;//返回索引对应的元素
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
Node<E> node(int index) {
if (index < (size >> 1)) {//如果索引在链表的前一半,从头结点向后查找
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else { //如果索引在链表的后一半,从尾结点向前查找
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
三、题目:【可以考虑双指针思想】
1、ArrayList与LinkedList区别:
1)ArrayList是基于数组实现的,支持随机访问,但是插入删除效率不如LinkedList。LinkedList是基于双链表实现的,支持顺序访问,适合频繁插入删除。
2)LinkedList比ArrayList更占内存,因为它除了存储数据,还要存储指针。
2、反转单链表:
/*
* 从前往后遍历反转单链表:head为头结点
*/
public static Node reverse(Node head) {
// 如果单链表为空或只有一个结点,不用反转
if (head == null || head.next == null) {
System.out.println("单链表为空或只有一个结点,不用反转");
return head;
}
Node node = head;
Node pre = null;
while (node != null) {
Node temp = node.next;// temp临时存储下一结点
node.next = pre;
pre = node;
node = temp;// 最后temp给node
}
return pre;// 返回反转后的单链表
}
示例图如下:
3、找出单向链表中倒数第 k 个节点
/*
* 单链表head中倒数第k个结点(k有效)的值:
* 双指针pre、cur:其中cur为head向后走k步,此时pre和cur同走直到链表尾部。pre即为倒数第k结点
*/
public static int findK(Node head, int k) {
Node pre = head;
Node cur = head;
for (int i = 0; i < k; i++) {
cur = cur.next;
}
while (cur != null) {
cur = cur.next;
pre = pre.next;
}
return pre.data;
}
4、删除单链表的中间结点
/*
* 删除链表的中间结点(链表长度至少为2)
* 双指针:pre慢指针,每一跳一步;cur快指针,每次两步。
*/
public static Node deleteNode(Node head) {
if (head == null || head.next == null) {
System.out.println("链表为空或只有一个结点,不用删除");
return head;
}
if (head.next.next == null) {
System.out.println("只有两个结点,直接删除第一个即可");
head = head.next;
return head;
}
Node pre = head;
Node cur = head;
Node temp = null;
while (cur.next != null && cur.next.next != null) {
cur = cur.next.next;// 每次跳两步
temp = pre;
pre = pre.next;// 每次跳一步
}
// pre就是中间结点,temp是其前驱
temp.next = pre.next;
return head;
}