链表常见问题:
- 反转单链表
- 随机链表的复制
- 链表-奇数位升序偶数位降序-让链表变成升序
- bucket如果用链表存储,它的缺点是什么?
- 如何判断链表的检测环
线性表的抽象接口描述:
public interface Ilist {
//将存在的线性表置成空表
void clear();
//判断线性表是否为空
boolean isEmpty();
//求线性表中的数据元素个数(求线性表的长度)
int length();
//读取并返回线性表中的第i个数据元素的值
Object get(int i);
//在线性表的第i个数据元素之前插入一个值为x的数据元素
void insert(int i, Object x);
//删除并返回线性表中第i个数据元素
void remove(int i);
//返回线性表中首次出现指定数据元素的位序号
int indexOf(Object x);
//输出线性表中各个元素的值
void display();
}
1.线性表的顺序存储
可以用数组来描述数据元素中的顺序存储结构,其中数组元素的个数对应存储区域的大小,假设为maxSize。考虑到线性表的长度是可变的,故还需要用一个变量curLen来记录线性表的实际长度。线性表的顺序存储结构在线性表Java接口的实现类如下:
public class SqList implements Ilist {
private Object[] listElem; //线性表存储空间
private int curLen; //线性表的当前长度
.
.
.
}
根据上述描述,对于线性表(34,12,25,61,30,49),其顺序存储结构如图:
1.1 顺序表类的描述如下:
public class SqList implements Ilist {
private Object[] listElem; //线性表存储空间
private int curLen; //线性表的当前长度
public SqList(int maxSize){
curLen = 0;
listElem = new Object[maxSize];
}
@Override
public void clear() {
curLen = 0;
}
@Override
public boolean isEmpty() {
return curLen == 0;
}
@Override
public int length() {
return curLen;
}
/**
* 读取并返回线性表中的第i个数据元素的值
* */
@Override
public Object get(int i) throws Exception {
if(i < 0 || i > curLen-1 ){
throw new Exception("该元素不存在");
}
return listElem[i];
}
/**
* 在线性表的第i个数据元素之前插入一个值为x的数据元素
* */
@Override
public void insert(int i, Object x) throws Exception {
if(curLen == listElem.length)
throw new Exception("顺序表已满");
if(i < 0 || i > curLen)
throw new Exception("插入位置不正确");
for(int j = curLen; j > i; j--)
listElem[j] = listElem[j-1];
listElem[i] = x;
curLen ++;
}
/**
* 删除并返回线性表中第i个数据元素
* */
@Override
public void remove(int i) throws Exception {
if(i < 0 || i > curLen-1)
throw new Exception("删除位置不正确");
for (int j = i; j< curLen-1; j++)
listElem[j] = listElem[j+1];
curLen --;
}
/**
* 返回线性表中首次出现指定数据元素的位序号
* */
@Override
public int indexOf(Object x) {
int j = 0;
while (j < curLen && !listElem[j].equals(x)) //依次比较
j++;
if(j < curLen)
return j;
else
return -1;
}
@Override
public void display() {
for (int j = 0; j < curLen; j++) {
System.out.println(listElem[j]);
}
}
}
1.2 顺序存储结构的优缺点:
- 优点:
无需为表示表中元素之间的逻辑关系而增加额外的存储空间;
可以快速地存取表中任一位置的元素。 - 缺点:
- 插入和删除操作需要移动大量元素;
- 当线性表长度变化较大时,难以确定存储空间的容量;
- 造成存储空间的“碎片”
2.线性表的链式存储
2.1单链表
头指针与头结点的异同:
头指针:
- 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
- 头指针具有标识作用,所以常用头指针冠以链表的名字
- 无论链表是否为空,头指针均不为空。头指针是链表的必要元素
头结点:
- 头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域般无意义(也可存放链表的长度)
- 有了头结点,对在第元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了
- 头结点不一定是链表必须要素
单链表是由若干个结点连接而成的,因此,要实现单链表,首先要设计结点类。其中,data是数据域,用来存放数据元素的值;next是指针域,用来存放后继结点的地址。
public class Node {
public Object data;
public Node next;
public Node(){
this(null,null);
}
public Node(Object data){
this(data,null);
}
public Node(Object data, Node next){
this.data = data;
this.next = next;
}
}
2.1.1 单链表类的描述
public class LinkList implements Ilist {
public Node head; //单链表的头指针
public LinkList(){ //单链表的构造函数
head = new Node(); //初始化头结点
}
/**
* 构造一个长度为n的单链表
* */
public LinkList(int n) throws Exception {
this(); //初始化头结点
Scanner s = new Scanner(System.in);
for (int j = 0; j < n; j++) {
//头插法
insert(0,s.next());
//尾插法
// insert(length(),s.next());
}
}
@Override
public void clear() {
head.data = null;
head.next = null;
}
@Override
public boolean isEmpty() {
return head.next == null;
}
@Override
public int length() {
Node p = head.next;
int length = 0;
if(p != null){
p = p.next;
++ length;
}
return length;
}
/**
* 读取带头结点的单链表中的第i个结点
* */
@Override
public Object get(int i) throws Exception {
Node p = head.next;
int j = 0;
while (p != null && j< i){
p = p.next;
++ j;
}
if(j > i || p == null) {
throw new Exception("元素不存在");
}
return p.data;
}
/**
* 在带头结点的单链表中的第i个结点之前插入一个值为x的新结点
* */
@Override
public void insert(int i, Object x) throws Exception {
Node p = head;
int j = -1;
while (p != null && j < i-1){
p = p.next;
++ j;
}
if(j > i-1 || p == null){
throw new Exception("插入位置不合法");
}
Node s = new Node(x);
s.next = p.next;
p.next = s;
}
/**
* 在不带头结点的单链表中的第i个结点之前插入一个值为x的新结点
* */
public void insert1(int i, Object x) throws Exception{
Node p = head;
int j = 0;
while (p != null && j < i-1){
p = p.next;
++ j;
}
if(j > i || p == null){
throw new Exception("插入位置不合法");
}
Node s = new Node(x);
if(i == 0){
s.next = head;
head = s;
}else {
s.next = p.next;
p.next = s;
}
}
/**
* 删除带头结点的单链表中的第i个结点
* */
@Override
public void remove(int i) throws Exception {
Node p = head;
int j = -1;
while (p.next != null && j < i-1){
p = p.next;
++ j;
}
if(j > i-1 || p.next == null){
throw new Exception("删除位置不合法");
}
p.next = p.next.next;
}
/**
* 在带头结点的单链表删除数据域值为x的结点
* */
public void remove1(Object x) throws Exception {
Node p = head;
while (p.next != null && !x.equals(p.next.data)){
p = p.next;
}
if(p.next == null){
throw new Exception("删除结点不存在");
}
p.next = p.next.next;
}
/**
* 在带头结点的单链表中查找值为x的结点
* */
@Override
public int indexOf(Object x) {
Node p = head.next;
int j = 0;
while (p != null && !p.data.equals(x)){
p = p.next;
++ j;
}
if(p != null)
return j;
else
return -1;
}
@Override
public void display() {
System.out.println("单链表为:");
Node node = head.next;
while (node != null){
System.out.print(node.data+" ");
node = node.next;
}
System.out.println();
}
/**
* 逆置单链表
* */
public void reserve(){
Node p = head.next;
Node q;
head.next = null;
while (p != null){
q = p.next;
p.next = head.next; //将p节点插入到链表的表头
head.next = p;
p = q;
}
}
/**
* 把单链表减半(留1 3 5 ...结点)
* 主单链表:1,2,3,4,5,6,7,8 ---> 1,3,5,7
* */
public void half(){
Node p = head.next;
while (p.next != null){
if(p.next.next != null){
p.next = p.next.next;
p = p.next;
}else {
p.next = null;
}
}
}
/**
* 把单链表分成两个链表,角标双数放一个表X,角标单数放一个表Y。(从0开始计数)
* 主单链表:1,2,3,4,5,6,7,8,9
* X:1,3,5,7,9
* Y:2,4,6,8
* */
public void split(LinkList X, LinkList Y){
Node p = head.next;
Node x = X.head;
Node y = Y.head;
//此时X就是整个主链表,把角标为单数的结点干掉就行
x.next = p;
//Y链表是从角标第一个开始的主链表,把角标为双数的结点干掉就行
y.next = p.next;
//当结点x和y的后第三位都不为空时,去掉第二个结点(x为空为y一定为空)
while (x.next.next.next != null && y.next.next.next != null){
x.next.next = x.next.next.next;
y.next.next = y.next.next.next;
x = x.next;
y = y.next;
}
//当主链表的结点数为单数时,y为空,x不为空,当主链表的结点数为双数时,都为空
//x.next.next有区别,y.next.next都为null;
if(x.next.next.next != null)
x.next.next = x.next.next.next;
else
x.next.next = null;
y.next.next = null;
}
/**
* 单链表的交替。把主单链表和单链表N,交替取数,为一个新单链表
* A(主):1,2,3,4,5 N:a,b,c,d,e ---> A:1,a,2,b,3,c,4,d,5,e
* */
public void alternate(LinkList N){
Node p = head.next;
Node n = N.head.next;
if(p == null){
head.next = n;
}else {
while(p.next != null){
Node q = p.next;
Node m = n.next;
p.next = n;
n.next = q;
p = q;
n = m;
}
p.next = n;
}
}
}
Test:
public static void main(String[] args) throws Exception {
LinkList linkList = new LinkList(4); //input: 4 3 2 1
linkList.display(); //1 2 3 4
LinkList list2 = new LinkList();
list2.alternate(linkList);
list2.display(); //1 2 3 4
LinkList list3 = new LinkList(4); //input: d c b a
list3.display(); //a b c d
list3.alternate(linkList);
list3.display(); //a 1 b 2 c 3 d 4
linkList.display(); // 1 b 2 c 3 d 4
linkList.insert1(0, 100);
linkList.display(); //null 1 b 2 c 3 d 4
}
单链表结构和顺序存储结构的对比:
2.2循环链表
循环链表也称为环形链表,将单链表的最后一个结点的后继指针指向第一个结点。
在实际应用中往往使用尾指针来标识循环链表,这样无论是访问第一个结点还是访问最后一个结点其时间复杂度都是O(1)。
合并两个循环链表:
Node p = tailb.next;
tailb.next = taila.next;
taila.next = p.next;
2.3 双向链表
2.3.1 双向链表的结点类:
/**
* 双向链表的结点类
* */
public class DulNode {
public Object data;
public DulNode prior;
public DulNode next;
public DulNode(){
this(null);
}
public DulNode(Object data){
this.data = data;
this.prior = null;
this.next = null;
}
}
2.3.2 双向循环链表类的描述:
public class DulLinkList implements Ilist {
public DulNode head;
/**
* 构造只含一个头结点的双向循环链表
* */
public DulLinkList(){
head = new DulNode();
head.prior = head;
head.next = head;
}
/**
* 从表尾到表头逆向创建双向循环链表
* */
public DulLinkList(int n) throws Exception {
this(); //上面的构造函数
Scanner s = new Scanner(System.in);
for (int j = 0; j < n; j++) {
insert(j, s.next()); //顺序创建
// insert(0, s.next()); //逆向创建
}
}
/**
* 带头结点的双向循环链表中的插入操作
* */
@Override
public void insert(int i, Object x) throws Exception {
DulNode p = head.next;
int j = 0;
while (!p.equals(head) && j < i){
p = p.next;
++ j;
}
if(j != i && !p.equals(head)){
throw new Exception("插入位置不正确!");
}
DulNode s = new DulNode(x);
p.prior.next = s;
s.prior = p.prior;
s.next = p;
p.prior = s;
}
/**
* 在带头结点的双向循环链表中的删除操作 -------i= 0,head.next = head的情况呢?
* */
@Override
public void remove(int i) throws Exception {
DulNode p = head.next;
int j = 0;
while (!p.equals(head) && j < i){
p = p.next;
++ j;
}
if(j != i){
throw new Exception("删除位置不合法");
}
p.prior.next = p.next;
p.next.prior = p.prior;
}
@Override
public void display() {
DulNode node = head.next;
while (!node.equals(head)){
System.out.print(node.data+" ");
node = node.next;
}
System.out.println();
}
}
Test :
public static void main(String[] args) throws Exception {
DulLinkList dulLinkList = new DulLinkList(5); //input: 1 2 3 4 5
dulLinkList.display(); //1 2 3 4 5
dulLinkList.insert(10, 100);
dulLinkList.display(); //1 2 3 4 5 100
DulLinkList dulLinkList1 = new DulLinkList();
dulLinkList1.display(); // (空的)
dulLinkList1.insert(-1 ,222);
dulLinkList1.display(); //222
DulLinkList dulLinkList2 = new DulLinkList();
dulLinkList2.display(); // (空的)
dulLinkList2.insert(10 ,555);
dulLinkList2.display();//555
DulLinkList dulLinkList3 = new DulLinkList();
dulLinkList3.remove(0); //不会没有异常
dulLinkList3.display();
}