Go Data Structure and Algorithms - Queues

Keywords: Go Algorithm data structure

introduce

A queue is a special linear table. A queue allows data to be inserted at one end and retrieved at the other end, and the data in the queue follows the First In First Our (FIFO) rule. One end of the retrieved data is called the head of the queue, and the other end of the inserted data is called the end of the queue.

Queues can be divided into: sequential queue, circular queue, chain queue.

Sequential Queue

type Queue struct {
	MaxSize int
	Front   int
	Rear    int
	Element []int
}

// Initialize Queue
func (q *Queue) initQueue(maxSize int) {
	q.MaxSize = maxSize
	q.Element = make([]int, maxSize)
	q.Front = -1
	q.Rear = -1
}

// Determine if the queue is empty
func (q *Queue) isEmpty() bool {
	flag := false
	if q.Front == q.Rear {
		flag = true
	}
	return flag
}

// Add an element to the queue
func (q *Queue) add(data int) (err error) {
	// Full Judgement
	if q.Rear == q.MaxSize-1 {
		err = errors.New("The queue is full and cannot be added!")
		return err
	}
	q.Rear++
	q.Element[q.Rear] = data
	return
}

// Get an element of the queue
func (q *Queue) poll() (data int, err error) {
	if q.isEmpty() {
		err = errors.New("the queue is empty")
		return
	}
	q.Front++
	data = q.Element[q.Front]
	return data, err
}

// View all elements of the queue
func (q *Queue) list() {
	for i := q.Front + 1; i < len(q.Element)-1; i++ {
		fmt.Printf("element[%d]: %d\n", i, q.Element[i])
	}
}
func main() {
	queue := Queue{}
	var key int
	var value int
	var size int
	for {

		fmt.Println("0. Initialize Queue")
		fmt.Println("1. Add data to queue")
		fmt.Println("2. Get data from a queue")
		fmt.Println("3. Display queue data")
		fmt.Println("4. Exit the program")
		fmt.Println("Please select (0)-4): ")
		fmt.Scanf("%d\n", &key)
		switch key {
		case 0:
			fmt.Println("Please enter the capacity of the initialization queue")
			fmt.Scanf("%d\n", &size)
			queue.initQueue(size)
		case 1:
			fmt.Println("Enter the data you want to add:")
			fmt.Scanf("%d\n", &value)
			err := queue.add(value)
			if err != nil {
				fmt.Println(err)
				return
			}
		case 2:
			data, err := queue.poll()
			if err != nil {
				fmt.Println(err)
				return
			}
			fmt.Printf("Data taken from the queue is: %d\n", data)
		case 3:
			queue.list()
		case 4:
			os.Exit(0)
		}
	}
}

Sequential queues can cause false overflow because their storage units do not have a mechanism for reuse. Therefore, the space utilization of sequential queues is not too high. The solution is to design sequential queues as a circular structure.

Circular Queue

A circular queue logically connects the end and end of the queue. This allows the location of the data removed to continue to be used in the next cycle, improving the space utilization of the queue.

The following points need attention:

  1. Both the queue header and queue tail subscripts start at 0. For recycling purposes, subscripts are not suitable to start at -1 as sequential queues do.
  2. When the head and tail of the queue are equal, the queue is empty.
  3. When (rear + 1)% maxSize = front, the queue is full. Plus one is because there is an empty position at the end and the head of the queue. Modifying maxSize is for looping. Actual mocking is used in many places to achieve looping.
  4. The number of elements in the queue is: (rear+maxSize-front)% maxSize.

    For example, the head of the team is 3 and the end of the team is 1: the maximum length is 5
    (rear+maxSize-front) % maxSize=(1+5-3)%5=3

  5. A position is empty between the queue head and the queue tail because the pointer at the queue tail points to the next unused position. If you do not empty a position, then the situation where the queue tail pointer points to the queue head pointer occurs. This conflicts with Point 2.
type CircleQueue struct {
	MaxSize int
	Front   int
	Rear    int
	Element []int
}

// Initialize Queue
func (cq *CircleQueue) initCircleQueue(maxSize int) {
	cq.MaxSize = maxSize
	cq.Element = make([]int, maxSize)
	cq.Front = 0
	cq.Rear = 0
}

// Determine if the queue is empty
func (cq *CircleQueue) isEmpty() bool {
	flag := false
	if cq.Front == cq.Rear {
		flag = true
	}
	return flag
}

// Determine if the queue is full
func (cq *CircleQueue) isFull() bool {
	flag := false
	if (cq.Rear+1)%cq.MaxSize == cq.Front {
		flag = true
	}
	return flag
}

// Insert a Data
func (cq *CircleQueue) add(data int) (err error) {
	if cq.isFull() {
		err = errors.New("the queue is full")
		return
	}
	cq.Element[cq.Rear] = data
	cq.Rear = (cq.Rear + 1) % cq.MaxSize
	return
}

// Take out a data
func (cq *CircleQueue) poll() (data int, err error) {
	if cq.isEmpty() {
		err = errors.New("the queue is empty")
		return
	}
	data = cq.Element[cq.Front]
	cq.Front = (cq.Front + 1) % cq.MaxSize
	return data, err
}

// Show all values in the queue
func (cq *CircleQueue) list() {
	if cq.isEmpty() {
		fmt.Println("the queue is empty")
		return
	}
	// Set an auxiliary traversal because the header of the queue cannot be changed
	tempFront := cq.Front
	for i := 0; i < (cq.Rear+cq.MaxSize-cq.Front)%cq.MaxSize; i++ {
		fmt.Printf("arr[%d]=%d\n", tempFront, cq.Element[tempFront])
		tempFront = (tempFront + 1) % cq.MaxSize
	}
}

Chained Queue

Comparison of chained and circular queues:

  • Time: All O(1)
  • Spatially: A circular queue requests space in advance and does not release during use. For a chain queue, it takes some time for each request and release node, but it is more flexible in space use.
  • Cyclic queues are preferred when the maximum queue length can be determined, or chained queues are preferred.
// Queue Node
type Node struct {
	element int
	next    *Node
}

// Queue Chain List
type LinkedQueue struct {
	Front  *Node
	Rear   *Node
	length int
}

// Initialize Queue
func (lq *LinkedQueue) initLinkedQueue() {
	lq.Front = nil
	lq.Rear = nil
	lq.length = 0
	fmt.Printf("Initialization succeeded!\n")
}

// Determine if the queue is empty
func (lq *LinkedQueue) isEmpty() bool {
	flag := false
	if lq.length == 0 {
		flag = true
	}
	return flag
}

// Entry
func (lq *LinkedQueue) add(data int) {

	// Construct a new node
	node := Node{element: data}

	// Determine if this is the first insert
	if lq.isEmpty() {
		lq.Front = &node
		lq.Rear = &node
	} else {
		lq.Rear.next = &node
		lq.Rear = &node
	}
	lq.length++
}

// Queue
func (lq *LinkedQueue) poll() (data int, err error) {
	if lq.isEmpty() {
		err = errors.New("the quenen is empty")
		return
	}

	// Remove data from node
	data = lq.Front.element

	// Handle the case where the last node is left
	if lq.length == 1 {
		lq.Front = nil
		lq.Rear = nil
		lq.length--
		return
	}

	lq.Front = lq.Front.next
	lq.length--

	return data, err
}

// View all elements of the queue
func (lq *LinkedQueue) list() {
	if lq.isEmpty() {
		fmt.Println("the queue is empty")
		return
	}
	// Because the queue head cannot be changed, all temporary variables are used instead
	tempFront := lq.Front
	for i := 0; i < lq.length; i++ {
		fmt.Println(tempFront.element)
		tempFront = tempFront.next
	}
}

Last

The complete code is here: https://github.com/bigzoro/go_algorithm/tree/main/queue

Posted by davejj on Fri, 15 Oct 2021 09:25:47 -0700